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

dapp caching #1406

Merged
merged 16 commits into from
Feb 11, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
23 changes: 7 additions & 16 deletions app/dapp/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ class App extends React.Component {
return (
<div className='splash'>
<Native />
<div className='overlay' />
<div className='mainLeft'>
{/* <div className='overlay' /> */}
{/* <div className='mainLeft'>
<div
className='accountTile'
onClick={() => {
Expand All @@ -64,36 +64,27 @@ class App extends React.Component {
})}
</div>
</div>
</div> */}
</div>
</div>
</div> */}
<div className='main'>
<div className='mainTop' />
{currentDapp ? (
<>
<div
{/* <div
className='mainDappBackground'
style={{
background: currentDapp.colors ? currentDapp.colors.background : 'none'
}}
>
<div className='mainDappBackgroundTop' />
{!currentView.ready ? (
<div className='mainDappLoading'>
<div className='loader' style={loaderStyle} />
</div>
) : null}
</div>
</div> */}
</>
) : !currentView.ready ? (
sendDapp.status === 'failed' ? (
<div className='mainDappLoading'>
<div className='mainDappLoadingText'>{'Send dapp failed to load'}</div>
</div>
) : (
<div className='mainDappLoading'>
<div className='loader' />
</div>
)
) : null
) : null}
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion app/dapp/index.dev.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
<link rel="stylesheet" href="./index.styl" type="text/css" />
</head>
<body>
<div class="frameOverlay"></div>
floating marked this conversation as resolved.
Show resolved Hide resolved
<div id="dapp"></div>
<script type="module" src="./index.js" nonce="8c7d2664-ae99-42c2-ae12-3304e9168f71"></script>
</body>
Expand Down
1 change: 0 additions & 1 deletion app/dapp/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
<link rel="stylesheet" href="./index.styl" type="text/css" />
</head>
<body>
<div class="frameOverlay"></div>
<div id="dapp"></div>
<script type="module" src="./index.js" nonce="2f0d956b-0d9c-4f1e-874a-57a1ec828872"></script>
</body>
Expand Down
1 change: 0 additions & 1 deletion app/dapp/index.styl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ body
right 0px
bottom 0px
z-index 0
background var(--ghostZ)

.mainLeft
position absolute
Expand Down
74 changes: 69 additions & 5 deletions main/dapps/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { app } from 'electron'
import path from 'path'
import { Readable } from 'stream'
import { hash } from 'eth-ens-namehash'
import log from 'electron-log'
import crypto from 'crypto'
import tar from 'tar-fs'

import store from '../store'
import nebulaApi from '../nebula'
Expand All @@ -9,6 +13,20 @@ import extractColors from '../windows/extractColors'

const nebula = nebulaApi()

class DappStream extends Readable {
constructor(hash: string) {
super()
this.start(hash)
}
async start(hash: string) {
for await (const buf of nebula.ipfs.get(hash, { archive: true })) {
this.push(buf)
}
this.push(null)
}
_read() {}
}

function getDapp(dappId: string): Dapp {
return store('main.dapps', dappId)
}
Expand All @@ -28,10 +46,43 @@ async function getDappColors(dappId: string) {
}
}

const cacheDapp = async (dappId: string, hash: string) => {
return new Promise((resolve, reject) => {
try {
const dir = path.join(app.getPath('userData'), 'DappCache')
const dapp = new DappStream(hash)
dapp.pipe(
tar
.extract(dir, {
map: (header) => {
header.name = path.join(dappId, ...header.name.split('/').slice(1))
return header
}
})
.on('finish', async () => {
try {
await getDappColors(dappId)
resolve(dappId)
} catch (e) {
reject(e)
}
})
)
} catch (e) {
reject(e)
}
})
}

// TODO: change to correct manifest type one Nebula version with types are published
async function updateDappContent(dappId: string, contentURI: string, manifest: any) {
// TODO: Make sure content is pinned before proceeding
store.updateDapp(dappId, { content: contentURI, manifest })
try {
// Create a local cache of the content
await cacheDapp(dappId, contentURI)
store.updateDapp(dappId, { content: contentURI, manifest })
} catch (e) {
log.error('error updating dapp cache', e)
}
}

let retryTimer: NodeJS.Timeout
Expand All @@ -46,12 +97,12 @@ async function checkStatus(dappId: string) {
log.info(`resolved content for ${dapp.ens}, version: ${version}`)

store.updateDapp(dappId, { record: resolved.record })

// TODO: Add case here to also run an update if the maifest doesn't match the local dir
if (dapp.content !== resolved.record.content) {
updateDappContent(dappId, resolved.record.content, resolved.manifest)
}

if (!dapp.colors) getDappColors(dappId)

store.updateDapp(dappId, { status: 'ready' })

// The frame id 'dappLauncher' needs to refrence target frame
Expand Down Expand Up @@ -97,6 +148,20 @@ store.observer(() => {
})
})

const refreshDapps = () => {
const dapps = store('main.dapps')
Object.keys(dapps || {}).forEach((id) => {
store.updateDapp(id, { status: 'loading' })
if (nebula.ready()) {
checkStatus(id)
} else {
nebula.once('ready', () => checkStatus(id))
}
})
}

setInterval(() => refreshDapps(), 1000 * 60 * 60)

let nextId = 0
const getId = () => (++nextId).toString()

Expand Down Expand Up @@ -142,7 +207,6 @@ const surface = {
}

server.sessions.add(ens, session)

store.addFrameView(frameId, view)
} else {
store.updateDapp(dappId, { ens, status: 'initial', openWhenReady: true })
Expand Down
101 changes: 20 additions & 81 deletions main/dapps/server/asset/index.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,35 @@
import cheerio from 'cheerio'
import log from 'electron-log'
import fs from 'fs'
import path from 'path'
import { app } from 'electron'

import nebulaApi from '../../../nebula'
import store from '../../../store'
import getType from './getType'
import { ServerResponse } from 'http'

const nebula = nebulaApi()

function error(res: ServerResponse, code: number, message: string) {
res.writeHead(code || 404)
res.end(message)
}

function getCid(namehash: string): string {
return store(`main.dapps`, namehash, `content`)
}
const dappCache = path.join(app.getPath('userData'), 'DappCache')

export default {
stream: async (res: ServerResponse, namehash: string, path: string) => {
// Stream assets from IPFS back to the client
let found = false

const cid = getCid(namehash)

try {
for await (const chunk of nebula.ipfs.cat(`${cid}${path}`)) {
if (!found) {
res.setHeader('content-type', getType(path))
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type')
res.writeHead(200)

found = true
}

res.write(chunk)
stream: async (res: ServerResponse, namehash: string, asset: string) => {
if (asset === '/') asset = '/index.html'
const assetPath = path.join(dappCache, namehash, asset)
if (fs.existsSync(assetPath)) {
try {
const stream = fs.createReadStream(assetPath)
res.setHeader('content-type', getType(asset))
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type')
res.writeHead(200)
stream.pipe(res)
floating marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
console.error(e)
error(res, 404, (e as NodeJS.ErrnoException).message)
}

res.end()
} catch (e) {
// console.error(' --- ' + e.message)
error(res, 404, (e as NodeJS.ErrnoException).message)
}

// file.content.on('data', data => res.write(data))
// file.content.once('end', () => res.end())
// stream.on('data', file => {
// if (!file) return error(res, 404, 'Asset not found')
// res.setHeader('content-type', getType(path))
// res.setHeader('Access-Control-Allow-Origin', '*')
// res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type')
// res.writeHead(200)
// file.content.on('data', data => res.write(data))
// file.content.once('end', () => res.end())
// })
// stream.on('error', err => error(res, err.statusCode, `For security reasons, please launch this app from Frame\n\n(${err.message})`))
},
dapp: async (res: ServerResponse, namehash: string) => {
// Resolve dapp via IPFS, inject functionality and send it back to the client
// if (!ipfs return error(res, 404, 'IPFS client not running')
const cid = store('main.dapps', namehash, 'content')

try {
const index = await nebula.ipfs.getFile(`${cid}/index.html`)
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type')
res.writeHead(200)
const $ = cheerio.load(index)
res.end($.html())
} catch (e) {
log.error('could not resolve dapp', (e as NodeJS.ErrnoException).message)
} else {
error(res, 404, asset === '/index.html' ? 'Dapp not found' : 'Asset not found')
}
}
}

// ipfs.get(`${cid}/index.html`, (err, files) => {
// if (err) return error(res, 404, 'Could not resolve dapp: ' + err.message)
// res.setHeader('Set-Cookie', [`__app=${app}`, `__session=${session}`])
// res.setHeader('Access-Control-Allow-Origin', '*')
// res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type')
// res.writeHead(200)

// let file = files[0].content.toString('utf8')

// const $ = cheerio.load(file.toString('utf8'))
// $('html').prepend(`
// <script>
// const initial = ${JSON.stringify(storage.get(cid) || {})}
// ${inject}
// </script>
// `)
// res.end($.html())
// })
// }
6 changes: 1 addition & 5 deletions main/dapps/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ const server = http.createServer((req, res) => {
}

if (sessions.verify(ens, session)) {
if (url.pathname === '/') {
return asset.dapp(res, namehash)
} else {
return asset.stream(res, namehash, url.pathname)
}
return asset.stream(res, namehash, url.pathname)
} else {
res.writeHead(403)
return res.end('No dapp session, launch this dapp from Frame')
Expand Down
39 changes: 39 additions & 0 deletions main/dapps/verify/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// A modified version of ipfs-only-hash, https://github.com/alanshaw/ipfs-only-hash/issues/18

import { globSource, CID } from 'ipfs-http-client'
import { importer, UserImporterOptions } from 'ipfs-unixfs-importer'
import { MemoryBlockstore } from 'blockstore-core/memory'

const hash = async (content: any, options: UserImporterOptions = {}) => {
options.onlyHash = true
if (typeof content === 'string') {
content = [{ content: new TextEncoder().encode(content) }]
} else if (content instanceof Object.getPrototypeOf(Uint8Array)) {
content = [{ content }]
}
let lastCID
for await (const c of importer(content, new MemoryBlockstore(), options)) {
lastCID = c.cid
}
return lastCID
}

const hashFiles = async (path: string, options: UserImporterOptions) => {
const files = globSource(path, '**')
return await hash(files, options)
}

// const cidToHex = (cid: CID) => {
// return `0x${Buffer.from(cid.bytes.slice(2)).toString('hex')}`
// }

const getCID = async (path: string, isDirectory: boolean) => {
return await hashFiles(path, { cidVersion: 0, hidden: true, wrapWithDirectory: isDirectory })
}

export default {
async verifyDapp(path: string, manifestCID: string) {
const cid = await getCID(path, true)
return cid?.toString() === manifestCID
}
}
4 changes: 2 additions & 2 deletions main/store/state/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,11 @@ const initial = {
colorway: main('colorway', 'dark'),
colorwayPrimary: {
dark: {
background: 'rgb(21, 17, 23)',
background: 'rgb(26, 22, 28)',
text: 'rgb(241, 241, 255)'
},
light: {
background: 'rgb(224, 217, 233)',
background: 'rgb(240, 230, 243)',
text: 'rgb(20, 40, 60)'
}
},
Expand Down
3 changes: 3 additions & 0 deletions main/windows/frames/viewInstances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export default {

frameInstance.addBrowserView(viewInstance)

const dappBackground = store('main.dapps', view.dappId, 'colors', 'background')
if (dappBackground) frameInstance.setBackgroundColor(dappBackground)

viewInstance.setBounds({
x: 0,
y: fullscreen ? 0 : 32,
Expand Down
Loading