Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: new store method that uploads token with all the assets and metadata #56

Merged
merged 29 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1b26f4c
feat: store metadata in IPLD CBOR format
Mar 30, 2021
a5a1667
Merge remote-tracking branch 'origin/feat/storeMetadata' into feat/nf…
Gozala Mar 31, 2021
64c6f64
feat: store api
Gozala Apr 6, 2021
9aa4624
Apply suggestions from code review
Gozala Apr 6, 2021
6cb9476
chore: change Token.encode to return FormData
Gozala Apr 6, 2021
c1aff1f
chore: remove CID dependency
Gozala Apr 6, 2021
89b5f64
chore: add comment for `embed` field
Gozala Apr 6, 2021
90c3ed7
chore: rename setAt to setIn
Gozala Apr 6, 2021
5f499dc
chore: change how URLs are encoded/decoded
Gozala Apr 7, 2021
bca5cf4
chore: refine store api
Gozala Apr 7, 2021
b8ef5db
feat: implement backend part
Gozala Apr 8, 2021
8b42d97
fix: typo that cause type check to fail
Gozala Apr 8, 2021
e952844
fix: regression in pinata.js
Gozala Apr 8, 2021
c016e08
fix: remaining issues on the backend
Gozala Apr 9, 2021
d21ec4d
chore: update addresses
Gozala Apr 12, 2021
395a3db
chore: merge main branch
Gozala May 3, 2021
c8f35b7
chore: merge main
Gozala May 5, 2021
7650543
chore: update implementation to use a cluster
Gozala May 5, 2021
407ec0f
chore: remove redundunt code
Gozala May 5, 2021
a5413b9
chore: undo unintended test changes
Gozala May 5, 2021
bee00e5
fix: import
Gozala May 5, 2021
4232bd1
fix: leftovers from previous iteration
Gozala May 5, 2021
746e705
Apply suggestions from code review
Gozala May 6, 2021
ede8e94
chore: switch to just-safe-set
Gozala May 6, 2021
98d34f4
Merge branch 'feat/nft-meta-block' of github.com:ipfs-shipyard/nft.st…
Gozala May 6, 2021
9eaeffc
chore: Per review feedback remove CID dep
Gozala May 6, 2021
ffaa115
fix: pinByHash by providing content-type
Gozala May 6, 2021
09500ee
chore: add comment to why skipLibCheck is enabled
Gozala May 6, 2021
229deed
fix: use latest multiformats to fix ts lint err
May 7, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"ipld-in-memory": "8.0.0",
"mocha": "8.3.2",
"multicodec": "3.0.1",
"multiformats": "4.5.3",
"multiformats": "^7.0.0",
"@ipld/dag-cbor": "^5.0.0",
"multihashing-async": "2.1.2",
"nyc": "15.1.0",
"playwright-test": "2.1.0",
Expand Down
70 changes: 69 additions & 1 deletion client/src/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
* ```
* @module
*/

import * as API from './lib/interface.js'
import * as Token from './token.js'
Gozala marked this conversation as resolved.
Show resolved Hide resolved
import { fetch, File, Blob, FormData } from './platform.js'

/**
Expand Down Expand Up @@ -113,6 +113,64 @@ class NFTStorage {
}
}

/**
* @template {API.TokenInput} T
* @param {API.Service} service
* @param {T} data
* @returns {Promise<API.Token<T>>}
*/
static async store(
{ endpoint, token },
{ name, description, image, properties, decimals, localization }
) {
const url = new URL(`/store`, endpoint)
// Just validate that expected field are present
if (typeof name !== 'string') {
throw new TypeError(
'string property `name` identifying the asset is required'
)
}
if (typeof description !== 'string') {
throw new TypeError(
'string property `description` describing asset is required'
)
}

if (!(image instanceof Blob) || !image.type.startsWith('image/')) {
throw new TypeError(
'proprety `image` must be a Blob or File object with `image/*` mime type'
)
}
if (typeof decimals !== 'undefined' && typeof decimals !== 'number') {
throw new TypeError('proprety `decimals` must be an integer value')
}

const body = Token.encode({
name,
description,
image,
properties,
decimals,
localization,
})
const paths = new Set(body.keys())

const response = await fetch(url.toString(), {
method: 'POST',
headers: NFTStorage.auth(token),
body,
})

/** @type {API.StoreResponse<T>} */
const result = await response.json()

if (result.ok === true) {
const { value } = result
return Token.decode(value, paths)
} else {
throw new Error(result.error.message)
}
}
/**
* @param {API.Service} service
* @param {string} cid
Expand Down Expand Up @@ -261,6 +319,14 @@ class NFTStorage {
check(cid) {
return NFTStorage.check(this, cid)
}
/**
* @template {API.TokenInput} T
* @param {T} token
* @returns {Promise<API.Token<T>>}
*/
store(token) {
return NFTStorage.store(this, token)
}
}

/**
Expand All @@ -283,6 +349,8 @@ const decodeDeals = (deals) =>
}
})

const TokenModel = Token.Token
export { TokenModel as Token }
export { NFTStorage, File, Blob, FormData }

/**
Expand Down
175 changes: 174 additions & 1 deletion client/src/lib/interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import type { CID } from 'multiformats'

export type { CID }

/**
* Define nominal type of U based on type of T. Similar to Opaque types in Flow
*/
export type Tagged<T, Tag> = T & { tag?: Tag }

export interface Service {
endpoint: URL
token: string
Expand All @@ -10,9 +19,27 @@ export interface PublicService {
/**
* CID in string representation
*/
export type CIDString = string & {}
export type CIDString = Tagged<string, CID>

export interface API {
/**
* Stores given token and all the resources (in form of File or a Blob) it
* references along with a metadata JSON as specificed in (ERC-1155). The
* `token.image` must be either `File` or a `Blob` instance, which will be
* stored and corresponding content address URL will be saved in metadata
* JSON file under `image` field.
*
* If `token.properties` contain properties with `File` or `Blob` values those
* also get stored and their URLs will be saved in metadata json in their
* place.
*
* Note: URLs for `File` object will retain the name e.g. in case of
* `new File([bytes], 'cat.png', { type: 'image/png' })` it will look like
* `ipfs://bafy...hash/image/cat.png`. For `Blob` object URL will not have
* name or mime type instead it will look more like `ipfs://bafy...hash/image/blob`
*/
Gozala marked this conversation as resolved.
Show resolved Hide resolved
store<T extends TokenInput>(service: Service, token: T): Promise<Token<T>>

/**
* Stores a single file and returns a corresponding CID.
*/
Expand Down Expand Up @@ -137,3 +164,149 @@ export interface Pin {
}

export type PinStatus = 'queued' | 'pinning' | 'pinned' | 'failed'

/**
* This is an input used to construct the Token metadata as per EIP-1155
* @see https://eips.ethereum.org/EIPS/eip-1155#metadata
*/
export interface TokenInput {
Gozala marked this conversation as resolved.
Show resolved Hide resolved
/**
* Identifies the asset to which this token represents
*/
name: string
/**
* Describes the asset to which this token represents
*/
description: string
/**
* An `File` with mime type image/* representing the asset this
* token represents. Consider making any images at a width between `320` and
* `1080` pixels and aspect ratio between `1.91:1` and `4:5` inclusive.
*
* If `File` object is used, URL in the metadata will include a filename
* e.g. `ipfs://bafy...hash/cat.png`. If `Blob` is used URL in the metadata
* will not include filename or extension e.g. `ipfs://bafy...img/`
*/
Gozala marked this conversation as resolved.
Show resolved Hide resolved
image: Blob | File

/**
* The number of decimal places that the token amount should display - e.g.
* `18`, means to divide the token amount by `1000000000000000000` to get its
* user representation.
*/
decimals?: number

/**
* Arbitrary properties. Values may be strings, numbers, nested object or
* arrays of values. It is possible to provide a `File` or a `Blob` instance
* as property value, in which case it is stored on IPFS and metadata will
* contain URL to it in form of `ipfs://bafy...hash/name.png` or
* `ipfs://bafy...file/` respectively.
*/
Gozala marked this conversation as resolved.
Show resolved Hide resolved
properties?: Object
Gozala marked this conversation as resolved.
Show resolved Hide resolved

localization?: Localization
}

interface Localization {
/**
* The URI pattern to fetch localized data from. This URI should contain the
* substring `{locale}` which will be replaced with the appropriate locale
* value before sending the request.
*/
uri: string
/**
* The locale of the default data within the base JSON
*/
default: string
/**
* The list of locales for which data is available. These locales should
* conform to those defined in the Unicode Common Locale Data Repository
* (http://cldr.unicode.org/).
*/
locales: string[]
}

export interface Token<T extends TokenInput> {
/**
* CID for the token that encloses all of the files including metadata.json
* for the stored token.
*/
ipnft: CIDString
Gozala marked this conversation as resolved.
Show resolved Hide resolved

/**
* URL like `ipfs://bafy...hash/meta/data.json` for the stored token metadata.
Gozala marked this conversation as resolved.
Show resolved Hide resolved
*/
url: EncodedURL

/**
* Actual token data in ERC-1155 format. It matches data passed as `token`
* argument except Files/Blobs are substituted with corresponding `ipfs://`
* URLs.
*/
data: Encoded<T, [[Blob, URL]]>

/**
* Token data just like in `data` field except urls corresponding to
* Files/Blobs are substituted with IPFS gateway URLs so they can be
* embedded in browsers that do not support `ipfs://` protocol.
*/
embed(): Encoded<T, [[Blob, URL]]>
}

export type EncodedError = {
message: string
}
export type EncodedURL = Tagged<string, URL>

export type Result<X, T> = { ok: true; value: T } | { ok: false; error: X }

export interface EncodedToken<T extends TokenInput> {
ipnft: CIDString
url: EncodedURL
data: Encoded<T, [[Blob, EncodedURL]]>
}
export type StoreResponse<T extends TokenInput> = Result<
EncodedError,
EncodedToken<T>
>

/**
* Represents `T` encoded with a given `Format`.
* @example
* ```ts
* type Format = [
* [URL, { type: 'URL', href: string }]
* [CID, { type: 'CID', cid: string }]
* [Blob, { type: 'Blob', href: string }]
* ]
*
* type Response<T> = Encoded<T, Format>
* ```
*/
export type Encoded<T, Format extends Pattern<any, any>[]> = MatchRecord<
T,
Rule<Format[number]>
>

/**
* Format consists of multiple encoding defines what input type `I` maps to what output type `O`. It
* can be represented via function type or a [I, O] tuple.
*/
type Pattern<I, O> = ((input: I) => O) | [I, O]

export type MatchRecord<T, R extends Rule<any>> = {
[K in keyof T]: MatchRule<T[K], R> extends never // R extends {I: T[K], O: infer O} ? O : MatchRecord<T[K], R> //Match<T[K], R>
? MatchRecord<T[K], R>
: MatchRule<T[K], R>
}

type MatchRule<T, R extends Rule<any>> = R extends (input: T) => infer O
? O
: never

type Rule<Format extends Pattern<any, any>> = Format extends [infer I, infer O]
? (input: I) => O
: Format extends (input: infer I) => infer O
? (input: I) => O
: never
4 changes: 2 additions & 2 deletions client/src/platform.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fetch, { Request, Response, Headers } from '@web-std/fetch'
import { FormData } from '@web-std/form-data'
import { Blob, ReadableStream } from '@web-std/blob'
import { File } from '@web-std/file'
import { ReadableStream } from '@web-std/blob'
import { File, Blob } from '@web-std/file'

export {
fetch,
Expand Down
Loading