From 28f87190a356f295b2b3d1ac03c74525695e557b Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sat, 22 Jan 2022 19:56:07 -0800 Subject: [PATCH] add readme, fix types + bugs, add example file --- README.md | 19 +++++++++++++++ example.js | 40 ++++++++++++++++++++++++++++++++ index.d.ts | 2 +- index.js | 59 +++++++++++++++++++++++++++++------------------ package-lock.json | 2 +- package.json | 4 ++-- 6 files changed, 100 insertions(+), 26 deletions(-) create mode 100644 README.md create mode 100644 example.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..72157ab --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# 🎁 ar-wrapper +A thin wrapper around [`arweave-js`](https://github.com/ArweaveTeam/arweave-js) for versioned permaweb document management. +Helps to abstract away complexity for document storage for servers which front +transaction + gas costs for users. Includes local caching for optimistic transaction executions. + +Usage of this library requires possession of a Arweave keyfile for a wallet which +has funds. + +You can look at `example.js` for annotated usage of this library. +Type definitions can be found in `index.d.ts`. + +### Installation +```bash +npm i ar-wrapper +``` + +```js +const { ArweaveClient } = require('ar-wrapper') +``` \ No newline at end of file diff --git a/example.js b/example.js new file mode 100644 index 0000000..c578698 --- /dev/null +++ b/example.js @@ -0,0 +1,40 @@ +const { ArweaveClient } = require('./index') + +// Address of your admin wallet with funds +const address = "..." +// Contents of your wallet JSON keyfile. Ideally do NOT hardcode this! Use environment variables or DI. +const keyfile = `{...}` + +// Main driver logic (using a main function to allow use of async-await features) +async function main() { + // Instantiate client + const client = new ArweaveClient(address, keyfile) + + // create a new document + const doc = await client.addDocument("Test Document", "Lorem Ipsum", { + "hasTag": true + }) + + // update document content (note the version bump!) + await doc.update("Woah, new content!") + + // see if content is cached locally + // version 0 is no longer cached! + console.log(`Is 'Test Document'@0 cached? ${client.isCached("Test Document", 0)}`) + // but version 1 is + console.log(`Is 'Test Document'@1 cached? ${client.isCached("Test Document", 1)}`) + + // fetch latest document content (if version is omitted, latest is fetched) + console.log(await client.getDocumentByName("Test Document")) + + // fetch specific version + // this is no longer in cache so will try to fetch from network + // this might take a long while (>2min) because of block confirmation times + // so will most likely timeout!! + console.log(await client.getDocumentByName("Test Document", 0)) + + // get specific document by transaction (that you know exists) + console.log(await client.getDocumentByTxId("v-KCk3wHsrJdCShnXigOZaJzMpddqKvdHVgdYoxB_8w")) +} + +main() \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 16b778b..5be4b78 100644 --- a/index.d.ts +++ b/index.d.ts @@ -57,6 +57,6 @@ declare module "ar-wrapper" { updateDocument(document: Document): Promise pollForConfirmation(txId: string, maxRetries?: number): Promise getDocumentByName(name: string, version?: number, maxRetries?: number, verifiedOnly?: boolean): Promise - getDocumentByName(txId: string, maxRetries?: number, verifiedOnly?: boolean): Promise + getDocumentByTxId(txId: string, maxRetries?: number, verifiedOnly?: boolean): Promise } } \ No newline at end of file diff --git a/index.js b/index.js index 1c8ca6e..23ed693 100644 --- a/index.js +++ b/index.js @@ -67,9 +67,8 @@ class Document { // Helper function to bump timestamp of document bumpTimestamp(dateMs) { - const options = { year: 'numeric', month: 'long', day: 'numeric' } const time = new Date(dateMs * 1000) - this.timestamp = time.toLocaleDateString('en-US', options) + this.timestamp = time.toString() } } @@ -91,9 +90,10 @@ class ArweaveClient { // Construct a new client given the address of the admin account, // keys to the wallet, and a set of options for connecting to an Arweave network. - // Options are identical to the ones supported by the official `arweave-js` library + // `cacheSize` can be set to 0 to disable caching (not recommended). + // Options are identical to the ones supported by the official `arweave-js` library. constructor(adminAddress, keyFile, cacheSize = 500, options = DEFAULT_OPTIONS) { - this.#key = keyFile + this.#key = JSON.parse(keyFile) this.adminAddr = adminAddress this.client = ArweaveLib.init(options) this.cache = new LRUMap(cacheSize) @@ -120,6 +120,7 @@ class ArweaveClient { // success, update doc data, add to cache doc.txID = tx.id + doc.posted = true this.cache.set(doc.name, doc) return doc } @@ -133,8 +134,8 @@ class ArweaveClient { } const cached = this.cache.get(documentName) - const versionMatch = desiredVersion ? cached.version === desiredVersion : true - return cached.synced && versionMatch + const versionMatch = desiredVersion !== undefined ? cached.version === desiredVersion : true + return cached.posted && versionMatch } // Add a new document @@ -179,17 +180,17 @@ class ArweaveClient { // Both names and versions are arrays. Use `verifiedOnly = false` to include // all submitted TXs (including ones from non-admin wallet accounts) #queryBuilder(names, versions, verifiedOnly = true) { - const tags = [{ - name: NAME, - values: names, - }] + const tags = [`{ + name: "${NAME}", + values: ${JSON.stringify(names)}, + }`] // versions is an optional field if (versions.length > 0) { - tags.push({ - name: VERSION, - values: versions, - }) + tags.push(`{ + name: "${VERSION}", + values: ${JSON.stringify(versions.map(n => n.toString()))}, + }`) } // TODO: handle pagination/cursor here @@ -197,7 +198,7 @@ class ArweaveClient { query: ` query { transactions( - tags: ${JSON.stringify(tags)}, + tags: [${tags.join(",")}], ${verifiedOnly ? `owners: ["${this.adminAddr}"]` : ""} ) { edges { @@ -218,6 +219,7 @@ class ArweaveClient { } } + // Return a document object via lookup by name async getDocumentByName(name, version, maxRetries = 10, verifiedOnly = true) { // check if doc is in cache and entry is up to date (and correct version) if (this.isCached(name, version)) { @@ -226,7 +228,7 @@ class ArweaveClient { // otherwise, fetch latest to cache // build query to lookup by name (and optionally version) and send request to arweave graphql server - const query = this.#queryBuilder([name], version ? [version] : [], verifiedOnly) + const query = this.#queryBuilder([name], version === undefined ? [] : [version], verifiedOnly) const req = await fetch('https://arweave.net/graphql', { method: 'POST', headers: { @@ -238,7 +240,7 @@ class ArweaveClient { const json = await req.json() // safe to get first item as we specify specific tags in the query building stage - const txId = version ? + const txId = version !== undefined ? json.data.transactions.edges[0]?.node.id : json.data.transactions.edges.sort((a, b) => { // we reverse sort edges if version is not defined to get latest version @@ -255,25 +257,26 @@ class ArweaveClient { return doc } + // Return a document object via lookup by transaction ID. Not cached. async getDocumentByTxId(txId, maxRetries = 10, verifiedOnly = true) { // ensure block with tx is confirmed (do not assume it is in cache) const txStatus = await this.pollForConfirmation(txId, maxRetries) // fetch tx metadata const transactionMetadata = await this.client.transactions.get(txId) - if (verifiedOnly && transactionMetadata.owner !== this.adminAddr) { + if (verifiedOnly && transactionMetadata.owner !== this.#key.n) { return Promise.reject(`Document is not verified. Owner address mismatched! Got: ${transactionMetadata.owner}`) } // tag parsing - const tags = transactionMetadata.get('tags').reduce((accum, tag) => { + const metaTags = transactionMetadata.get('tags').reduce((accum, tag) => { let key = tag.get('name', {decode: true, string: true}) accum[key] = tag.get('value', {decode: true, string: true}) return accum }, {}) // assert that these are actually documents - if (!(tags.hasOwnProperty(NAME) && tags.hasOwnProperty(VERSION))) { + if (!(metaTags.hasOwnProperty(NAME) && metaTags.hasOwnProperty(VERSION))) { return Promise.reject(`Transaction ${txId} is not a document. Make sure your transaction ID is correct`) } @@ -286,12 +289,24 @@ class ArweaveClient { string: true, }), ]) - const docData = JSON.parse(dataString) + const { + name, + content, + version, + tags + } = JSON.parse(dataString) // transform into document and return - const doc = new Document(this, docData, tags) + const doc = new Document(this, name, content, tags, version) + doc.posted = true + doc.txID = txId doc.bumpTimestamp(blockMeta.timestamp) this.cache.set(doc.name, doc) return doc } +} + +module.exports = { + ArweaveClient, + Document } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 00ce18f..10b7c58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "arweave": "^1.10.23", + "arweave": "^1.10.18", "cross-fetch": "^3.1.5", "exponential-backoff": "^3.1.0", "lru_map": "^0.4.1" diff --git a/package.json b/package.json index 6551b23..5c9aad7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ar-wrapper", "version": "1.0.0", - "description": "", + "description": "Thin wrapper around arweave-js for versioned permaweb document management", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" @@ -17,7 +17,7 @@ }, "homepage": "https://github.com/verses-xyz/ar-wrapper#readme", "dependencies": { - "arweave": "^1.10.23", + "arweave": "^1.10.18", "cross-fetch": "^3.1.5", "exponential-backoff": "^3.1.0", "lru_map": "^0.4.1"