Skip to content

Commit

Permalink
add readme, fix types + bugs, add example file
Browse files Browse the repository at this point in the history
  • Loading branch information
jackyzha0 committed Jan 23, 2022
1 parent ed50d68 commit 28f8719
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 26 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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')
```
40 changes: 40 additions & 0 deletions example.js
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ declare module "ar-wrapper" {
updateDocument(document: Document): Promise<Document>
pollForConfirmation(txId: string, maxRetries?: number): Promise<BlockStatusI>
getDocumentByName(name: string, version?: number, maxRetries?: number, verifiedOnly?: boolean): Promise<Document>
getDocumentByName(txId: string, maxRetries?: number, verifiedOnly?: boolean): Promise<Document>
getDocumentByTxId(txId: string, maxRetries?: number, verifiedOnly?: boolean): Promise<Document>
}
}
59 changes: 37 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand All @@ -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)
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -179,25 +180,25 @@ 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
return {
query: `
query {
transactions(
tags: ${JSON.stringify(tags)},
tags: [${tags.join(",")}],
${verifiedOnly ? `owners: ["${this.adminAddr}"]` : ""}
) {
edges {
Expand All @@ -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)) {
Expand All @@ -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: {
Expand All @@ -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
Expand All @@ -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`)
}

Expand All @@ -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
}
2 changes: 1 addition & 1 deletion package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand Down

0 comments on commit 28f8719

Please sign in to comment.