├── .gitignore ├── public ├── avatar.png ├── favicon.ico ├── unknown.png ├── bitpicpreview.png ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── index.html ├── show.ejs ├── about.html └── upload.html ├── package.json ├── README.md ├── planarium.js └── planaria.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | tape.txt 3 | chain 4 | files 5 | db 6 | node_modules 7 | -------------------------------------------------------------------------------- /public/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interplanaria/gridbitpic/HEAD/public/avatar.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interplanaria/gridbitpic/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interplanaria/gridbitpic/HEAD/public/unknown.png -------------------------------------------------------------------------------- /public/bitpicpreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interplanaria/gridbitpic/HEAD/public/bitpicpreview.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interplanaria/gridbitpic/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interplanaria/gridbitpic/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interplanaria/gridbitpic/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interplanaria/gridbitpic/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interplanaria/gridbitpic/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gridbitpic", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bitpic": "0.0.2", 13 | "bitquery": "^0.3.138", 14 | "cors": "^2.8.5", 15 | "ejs": "^3.0.1", 16 | "express": "^4.17.1", 17 | "gridplanaria": "0.1.9", 18 | "mingo": "^2.4.0", 19 | "neonplanaria": "0.0.41" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitpic 2 | 3 | > Your own avatar on bitcoin, forever. 4 | 5 | ![bitpic](public/avatar.png) 6 | 7 | bitpic is a protocol for hosting and using Paymail avatars on the Bitcoin blockchain. 8 | 9 | **You can think of it as [Gravatar](https://en.gravatar.com/), but for Bitcoin.** 10 | 11 | - Instead of using a normal email address, Bitpic uses [Paymail](https://bsvalias.org/) address. 12 | - Instead of storing the imagess and the image database on a proprietary server, it stores it on the Bitcoin blockchain. 13 | - The images are 100% stored on the Bitcoin blockchain, signed by Paymail user identity public key. Images signed with invalid signature are not indexed. 14 | 15 | Learn more about how it works here: [How Bitpic Works](https://bitpic.network/about) 16 | 17 | # Install 18 | 19 | ## 1. Install Bitcoin 20 | 21 | Download Bitcoin node. 22 | 23 | [Download Bitcoin](https://github.com/bitcoin-sv/bitcoin-sv/releases) 24 | 25 | ## 1. Install Docker 26 | 27 | Bitpic uses Docker. Install Docker first. 28 | 29 | [Install Docker](https://docs.docker.com/v17.09/engine/installation/#supported-platforms) 30 | 31 | ## 2. Clone this repository 32 | 33 | ``` 34 | git clone https://github.com/interplanaria/bitpic.git 35 | ``` 36 | 37 | # Usage 38 | 39 | ## 1. Run crawler 40 | 41 | The [gridplanaria.js](gridplanaria.js) file is the bitcoin crawler. 42 | 43 | ``` 44 | node gridplanaria 45 | ``` 46 | 47 | You may want to run it in the background by using [pm2](https://pm2.keymetrics.io/) or similar ways. 48 | 49 | 50 | ## 2. Run the web app + api 51 | 52 | The [planarium.js](planarium.js) file is the user facing website and api endpoint. 53 | 54 | This file powers everything you see at [https://bitpic.network](https://bitpic.network) and [https://bitpic.network/query](https://bitpic.network/query). 55 | 56 | ``` 57 | node planarium 58 | ``` 59 | 60 | Again, you may want to run it in the background by using [pm2](https://pm2.keymetrics.io/) or similar ways. 61 | -------------------------------------------------------------------------------- /planarium.js: -------------------------------------------------------------------------------- 1 | const { planarium, planaria } = require('neonplanaria') 2 | const bitquery = require('bitquery') 3 | const express = require('express') 4 | const fs = require('fs') 5 | const ejs = require('ejs') 6 | const cors = require('cors') 7 | const id = "bitpic" 8 | var template; 9 | fs.readFile("public/show.ejs", "utf-8", (err, str) => { 10 | console.log("str = ", str) 11 | template = ejs.compile(str); 12 | }) 13 | planarium.start({ 14 | name: "bitpic", 15 | port: 3004, 16 | onstart: async function() { 17 | let db = await bitquery.init({ url: "mongodb://localhost:27017", address: id }); 18 | return { db: db }; 19 | }, 20 | onquery: function(e) { 21 | let code = Buffer.from(e.query, 'base64').toString() 22 | let req = JSON.parse(code) 23 | if (req.q && req.q.find) { 24 | e.core.db.read(id, req).then(function(result) { 25 | e.res.json(result) 26 | }) 27 | } else { 28 | e.res.json([]) 29 | } 30 | }, 31 | custom: function (e) { 32 | e.app.use(express.static('public')) 33 | e.app.set('view engine', 'ejs'); 34 | e.app.use(cors()) 35 | e.app.get('/', (req, res) => { 36 | res.sendFile(process.cwd() + "/public/index.html") 37 | }) 38 | e.app.get('/about', (req, res) => { 39 | res.sendFile(process.cwd() + "/public/about.html") 40 | }) 41 | e.app.get('/upload', (req, res) => { 42 | res.sendFile(process.cwd() + "/public/upload.html") 43 | }) 44 | e.app.get('/me/:paymail', (req, res) => { 45 | // user avatar landing page 46 | res.set('Content-Type', 'text/html'); 47 | console.log(req.url) 48 | let url = req.originalUrl 49 | let r = template({ 50 | paymail: req.params.paymail, 51 | }) 52 | res.send(Buffer.from(r)) 53 | }) 54 | e.app.get('/u/:paymail', (req, res) => { 55 | // avatar serve 56 | res.setHeader("Content-Type","image/jpeg"); 57 | let filename = process.cwd() + "/files/" + req.params.paymail 58 | fs.access(filename, (err) => { 59 | if (err) { 60 | if (req.query.d) { 61 | res.redirect(req.query.d) 62 | return; 63 | } else { 64 | filename = process.cwd() + "/public/unknown.png" 65 | } 66 | } 67 | let filestream = fs.createReadStream(filename) 68 | filestream.on("error", function(e) { 69 | res.status(500).send(e.message); 70 | }); 71 | filestream.pipe(res); 72 | }) 73 | }) 74 | e.app.get('/exists/:paymail', (req, res) => { 75 | // 'exists' 76 | let filename = process.cwd() + "/files/" + req.params.paymail 77 | fs.access(filename, (err) => { 78 | if (err) { 79 | res.send("0") 80 | } else { 81 | res.send("1") 82 | } 83 | }) 84 | }) 85 | }, 86 | }) 87 | -------------------------------------------------------------------------------- /planaria.js: -------------------------------------------------------------------------------- 1 | const Planaria = require('gridplanaria') 2 | const MongoClient = require('mongodb') 3 | const mingo = require('mingo') 4 | const path = require('path') 5 | const bitpic = require('bitpic') 6 | const fs = require('fs') 7 | var db; 8 | const save = function(tx) { 9 | let hash = tx.tx.h; 10 | let ps = tx.out.map(function(out) { 11 | return new Promise(function(resolve, reject) { 12 | let buf = null 13 | let content 14 | if (out.tape.length > 2) { 15 | if (out.tape[1].cell[1].lb && typeof out.tape[1].cell[1].lb === 'string') { 16 | buf = Buffer.from(out.tape[1].cell[1].lb, 'base64'); 17 | } else if (out.tape[1].cell[1].b && typeof out.tape[1].cell[1].b === 'string') { 18 | buf = Buffer.from(out.tape[1].cell[1].b, 'base64'); 19 | } 20 | fs.writeFile(process.cwd() + '/files/' + out.tape[2].cell[1].s, buf, function(er) { 21 | if (er) { 22 | console.log("Error = ", er) 23 | reject() 24 | } else { 25 | resolve(out.tape[2].cell[1].s) 26 | } 27 | }) 28 | } else { 29 | resolve(null) 30 | } 31 | }) 32 | }) 33 | return Promise.all(ps) 34 | } 35 | const connect = function(cb) { 36 | MongoClient.connect("mongodb://localhost:27017", {useNewUrlParser: true}, function(err, client) { 37 | if (err) { 38 | console.log("retrying...") 39 | setTimeout(function() { 40 | connect(cb); 41 | }, 1000) 42 | } else { 43 | let id = "bitpic" 44 | db = client.db(id) 45 | cb(); 46 | } 47 | }) 48 | } 49 | const planaria = new Planaria(); 50 | var _filter = new mingo.Query({ 51 | "out.tape.cell.s": "18pAqbYqhzErT6Zk3a5dwxHtB9icv8jH2p" 52 | }) 53 | planaria.start({ 54 | filter: { 55 | //"from": 609000, 56 | "from": 609341, 57 | "host": { 58 | rpc: { user: "root", pass: "bitcoin" } 59 | }, 60 | "l": { 61 | filter: (e) => { 62 | let matched = ( 63 | e.out[0].tape.length > 2 && 64 | e.out[0].tape[2] && 65 | e.out[0].tape[2].cell[0] && 66 | e.out[0].tape[2].cell[0].s === "18pAqbYqhzErT6Zk3a5dwxHtB9icv8jH2p" ) || 67 | ( 68 | e.out.length > 1 && 69 | e.out[1].tape.length > 2 && 70 | e.out[1].tape[2] && 71 | e.out[1].tape[2].cell[0] && 72 | e.out[1].tape[2].cell[0].s === "18pAqbYqhzErT6Zk3a5dwxHtB9icv8jH2p" 73 | ) 74 | if (matched) console.log("Matched", JSON.stringify(e,null,2)) 75 | return matched 76 | } 77 | } 78 | }, 79 | onmempool: async function(e) { 80 | console.log("E = ", e) 81 | let valid = await bitpic.verify(e.tx, { format: "bob" }) 82 | if (valid) { 83 | let paymails = await save(e.tx) 84 | let paymail = paymails.filter((p) => { 85 | return p 86 | }) 87 | console.log("delete u") 88 | try { 89 | await db.collection("u").deleteMany({ 90 | "out.tape.cell.s": { 91 | "$in": paymail 92 | } 93 | }) 94 | console.log("delete c") 95 | await db.collection("c").deleteMany({ 96 | "out.tape.cell.s": { 97 | "$in": paymail 98 | } 99 | }) 100 | console.log("insert u", e.tx) 101 | await db.collection("u").insertMany([e.tx]) 102 | console.log("Inserted u") 103 | } catch (e) { 104 | console.log("Error", e) 105 | } 106 | } else { 107 | console.log("Invalid format", e.tx.tx.h) 108 | } 109 | }, 110 | onblock: function(e) { 111 | console.log("onblock") 112 | return new Promise((resolve, reject) => { 113 | let txArrayStream = e.tx(100) // Get a stream of transaction arrays 114 | console.time("block " + e.header.height) 115 | txArrayStream.on("data", async (txArray) => { 116 | txArrayStream.pause(); 117 | 118 | let invalidTxs = []; 119 | for(let i=0; i { 124 | return p 125 | }) 126 | await db.collection("c").deleteMany({ 127 | "out.tape.cell.s": { 128 | "$in": paymail 129 | } 130 | }) 131 | } else { 132 | console.log("Invalid format", txArray[i].tx.h) 133 | invalidTxs.push(txArray[i].tx.h) 134 | } 135 | } 136 | if (invalidTxs.length > 0) { 137 | console.log("Filtering invalid txs...", invalidTxs) 138 | txArray = txArray.filter((tx) => { 139 | return invalidTxs.includes(tx.tx.h) 140 | }) 141 | } 142 | let paymailSet = new Set(); 143 | let dedupTxArray = []; 144 | for(let i=txArray.length-1; i>=0; i--) { 145 | let tx = txArray[i]; 146 | let paymail = tx.out[0].tape[2].cell[1].s 147 | console.log("FOund paymail", paymail) 148 | if (!paymailSet.has(paymail)) { 149 | dedupTxArray.push(tx) 150 | } 151 | paymailSet.add(paymail) 152 | } 153 | txArray = dedupTxArray.reverse() 154 | console.log("inserting", txArray.length) 155 | try { 156 | await db.collection("c").insertMany(txArray) // insert each batch 157 | } catch (err) { 158 | console.log("Error = ", err) 159 | } 160 | console.log("inserted") 161 | txArrayStream.resume(); 162 | }) 163 | .on("end", () => { 164 | console.log("block End", e.header.height) 165 | db.collection("u").deleteMany({}).then(() => { 166 | console.timeEnd("block " + e.header.height) 167 | resolve() 168 | }) 169 | }) 170 | }) 171 | }, 172 | onstart: function(e) { 173 | return new Promise(async function(resolve, reject) { 174 | if (!e.tape.self.start) { 175 | await planaria.exec("docker", ["pull", "mongo:4.0.4"]) 176 | await planaria.exec("docker", ["run", "-d", "-p", "27017-27019:27017-27019", "-v", process.cwd() + "/db:/data/db", "mongo:4.0.4"]) 177 | } 178 | if (!fs.existsSync(process.cwd() + "/files")) { 179 | fs.mkdirSync(process.cwd() + "/files") 180 | } 181 | connect(async () => { 182 | console.log("creating index") 183 | await db.collection("c").createIndex({"tx.h": 1}, { unique: true}) 184 | await db.collection("c").createIndex({"blk.i": 1}) 185 | await db.collection("c").createIndex({"out.tape.cell.s": 1}) 186 | await db.collection("u").createIndex({"tx.h": 1}, { unique: true}) 187 | await db.collection("u").createIndex({"out.tape.cell.s": 1}) 188 | console.log("created index") 189 | if (e.tape.self.start) { 190 | await db.collection("c").deleteMany({ 191 | "blk.i": { "$gt": e.tape.self.end } 192 | }) 193 | resolve() 194 | } else { 195 | resolve(); 196 | } 197 | }) 198 | }) 199 | }, 200 | }) 201 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 161 | 162 | 218 | 219 | 220 |
221 | 222 | 223 |
224 | bitpic 225 |
226 |
Your own avatar on bitcoin, forever.
227 | About 228 | API 229 | GitHub 230 | Twitter 231 |
232 | click to upload yourself 233 |
234 |

Unconfirmed

235 |
236 |

Confirmed

237 |
238 |
239 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /public/show.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 232 | 233 | 234 | 235 | 236 | 237 | 299 | 300 | 301 |
302 | 303 | 304 |
305 | bitpic 306 |
307 |
Your own avatar on bitcoin, forever.
308 | How to Use 309 |
310 |
311 | 321 |
322 | update 323 |
324 | 325 |
326 |
327 | 328 | 329 | 332 |
333 | 334 |
335 | 336 | 337 | -------------------------------------------------------------------------------- /public/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 106 | 107 | 108 |
109 | 110 | 111 |
112 | bitpic 113 |
114 |
Your own avatar on bitcoin, forever.
115 |
116 |
117 |

What is Bitpic?

118 |

bitpic is a protocol for hosting and using Paymail avatars on the Bitcoin blockchain.

119 |

You can think of it as Gravatar, but for Bitcoin.

120 | 126 |
127 |

Step 1. Upload avatar to Bitcoin

128 |

Create a Bitcoin transaction which uploads a signed image using the Bitpic protocol.

129 |

130 |

131 | OP_0
132 | OP_RETURN
133 | 19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAut
134 |   [FILE Binary]
135 |   image/jpeg
136 |   binary
137 | |
138 | 18pAqbYqhzErT6Zk3a5dwxHtB9icv8jH2p
139 |   [Paymail]
140 |   [Pubkey]
141 |   [Sig]
142 | 
143 |

144 |
145 |

Here's an example:

146 |

Bitpic Transaction Explorer

147 |
148 |

Here's Bitcom Terminal (Bterm) for bitpic:

149 |

Bterm

150 |
151 |

Step 2. Use

152 |

Once uploaded, you can use the image from anywhere, simply by referencing: https://bitpic.network/u/[Paymail]

153 |

Here's an example:

154 | 155 | https://bitpic.network/u/644@moneybutton.com 156 |
157 | 158 |
159 |
160 |
161 |

Step 3. Update

162 |

Bitpic is a Planaria node which implements a mutable database and file system.

163 |

This means whenever you upload a new avatar, your bitpic address will be updated to serve the newly uploaded avatar image.

164 |
165 |

How does it work?

166 |

Bitpic is powered by Planaria, a Bitcoin crawler which indexes Bitcoin transactions in realtime into a database. 167 |

168 | 169 |


170 |

Why use Bitpic?

171 |
    172 |
  1. Your avatar forever: Paymail providers and wallet providers may disappear in hundreds of years, but your Bitpic avatar will never go away because it's on the blockchain. If you post something tied to your Paymail, you probably want the avatar to show up forever even after thousands of years. Bitpic allows that, because Bitpic is an infinite state machine powered by Planaria.
  2. 173 |
  3. App developers: No need to roll your own avatar system: As an application developer, you don't need to build a user avatar system from scratch. Just start using the avatar by referencing https://bitpic.network/u/[Paymail]! If an avatar doesn't exist for that paymail address, you will instead see the default image. For example, here's what https://bitpic.network/u/invalid@invalid.com looks like: 174 |
    175 | 176 | 177 | 178 |
  4. 179 |
  5. 100% Open: Bitpic is open source and powered by Planaria, which means anybody can run their own Bitpic Planaria node to operate the avatar system. In fact, you don't even have to use the https://bitpic.network/u/[Paymail] if you run your own Bitpic node!
  6. 180 |
  7. Interoperable and Permissionless: If you are an app developer or a wallet company, you only need to worry about implementing Bitpic on your side. You don't need to ask other wallets or applications for permission to use their avatars tied to their paymails, because the avatars are served from the single source of truth: the Bitcoin blockchain.
  8. 181 |
182 |
183 |

How to use Bitpic

184 |

1. For users

185 |

Once you upload your avatar to the blockchain, you can use it from anywhere. For example you can embed your Bitpic in your website simply with an HTML tag: <img src="https://bitpic.network/u/<YOUR_PAYMAIL>">

186 |

2. For app developers

187 |

No need to roll your own avatar system: As an app developer, you don't need to roll your own custom avatar system. Bitpic is an avatar owned by the user through Paymail. Outsource your Avatar system to the Bitcoin blockchain.

188 |

Uploading Avatars: Allowing your users to upload avatars is also easy. Whenever you need to allow your user to upload avatars, all you need to do is to send them to the Bitpic upload interface.

189 |

100% Customizable: The Bitpic upload interface is a server-less application made up of static HTML and JavaScript. Which means, if are a wallet provider, or an app developer, and you want your own custom Avatar uploading UI within your app, you can easily do so. Just take a look at the frontend code at Upload page, and build your own version which creates transactions that follow the protocol. Then you can run your own Bitpic planaria node, or simply use the default bitpic.network URL. Just make sure to say "Powered by Bitpic", so people know where they're uploading to.

190 |
191 |

Highlights

192 |

1. Powered by Planaria

193 |

Bitpic is powered by Planaria. The Bitpic planaria node creates a mutable database which updates the image tied to a paymail address.

194 |

2. 100% Open and Portable

195 |

Anyone can run a Bitpic avatar node because:

196 |
    197 |
  1. The avatar files are fully stored on-chain
  2. 198 |
  3. Bitpic is powered by Planaria
  4. 199 |
  5. The Bitpic Planaria node is open source
  6. 200 |
  7. The entire Bitpic frontend is open source
  8. 201 |
202 |
203 |

Endpoints

204 |
    205 |
  1. /u/<paymail> the image url you can embed in your apps. Example: https://bitpic.network/u/644@moneybutton.com
  2. 206 |
  3. /exists/<paymail> returns "1" if the paymail has an associated bitpic avatar. returns "0" if it doesn't exist yet.. Example: https://bitpic.network/exists/644@moneybutton.com
  4. 207 |
208 |
209 |

Default Image

210 |

By default Bitpic returns a fixed default image if an image doesn't yet exist for the corresponding paymail address. Here's what it looks like:

211 | 212 |

However it is possible to specify your own default image when the image doesn't load. You can do this by adding a d=<default image url> parameter at the end. Here's an example:

213 |

https://bitpic.network/u/fake@fake.com?d=https://thumbs.gfycat.com/ExhaustedSaneAngwantibo-small.gif

214 |

215 |
216 | 217 | 218 | -------------------------------------------------------------------------------- /public/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 486 | 487 | 488 |
489 | 490 | 491 |
492 | bitpic 493 |
494 |
Your own avatar on bitcoin, forever.
495 | How it works 496 |
497 | 498 | 499 |
500 | 504 |
505 | 509 | 513 | 519 |
520 | 521 | 522 | --------------------------------------------------------------------------------