├── src ├── web │ ├── home.js │ ├── helpers.js │ ├── main.scss │ ├── routes.js │ ├── index.js │ ├── main.js │ └── invoice.js ├── common │ ├── utils.js │ └── validators.js ├── contracts │ ├── index.js │ ├── eth_provider.js │ └── abi │ │ └── ERC20.json ├── server │ ├── index.js │ ├── jobs │ │ ├── callbacks.js │ │ └── invoices.js │ ├── api_v1.js │ ├── server.js │ └── invoice │ │ └── manager.js └── scripts │ └── wallets.js ├── .gitignore ├── CHANGELOG.md ├── .babelrc ├── tools ├── scripts.webpack.config.js └── app.webpack.config.js ├── config └── default.cson ├── LICENSE ├── test ├── ERC20_ABI.json ├── test.js └── ERC20.bin ├── package.json └── README.md /src/web/home.js: -------------------------------------------------------------------------------- 1 | import { h } from 'hyperapp' 2 | 3 | export default { 4 | Init: state => state, 5 | View: _ => ( 6 |

DECENTRAPAY

7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | *.log 3 | *.orig 4 | /.tmp 5 | /node_modules 6 | /dist 7 | npm-debug.* 8 | static/runtime* 9 | static/main* 10 | static/vendor* 11 | static/media 12 | static/index* 13 | static/*.css 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | **1.1.0** - migration to Hyperapp v2, PoachDB, support for multiple stablecoins 2 | 3 | **1.0.1** - added tool for collecting payments from deposit addresses generated by Daipay 4 | 5 | **1.0.0** - first release 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/web/helpers.js: -------------------------------------------------------------------------------- 1 | import { h } from 'hyperapp' 2 | 3 | export function classNames(arr) { 4 | return arr.join(' ') 5 | } 6 | 7 | 8 | export function displayCurrency(amount, currency) { 9 | return {amount.toFixed(2)} {currency} 10 | } 11 | -------------------------------------------------------------------------------- /src/common/utils.js: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js' 2 | 3 | export function fromBN(val) { 4 | return new BigNumber(val).dividedBy(Math.pow(10,18)).toNumber() 5 | } 6 | 7 | export function toBN(val) { 8 | return BigNumber(val).multipliedBy(Math.pow(10,18)) 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/contracts/index.js: -------------------------------------------------------------------------------- 1 | import ethProvider from './eth_provider' 2 | import { ethers } from 'ethers' 3 | import config from 'config' 4 | import DAI_ABI from './abi/ERC20.json' 5 | 6 | export const DAI = new ethers.Contract(config.public.contracts.DAI.address, DAI_ABI, ethProvider) 7 | 8 | export { ethProvider } 9 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import app from './server' 2 | import config from 'config' 3 | import { runLastBlockJob } from './jobs/invoices' 4 | import { runCallbackJob } from './jobs/callbacks' 5 | 6 | app.listen(config.listen, _ => console.log("Listening on port",config.listen)) 7 | 8 | runLastBlockJob() 9 | runCallbackJob() 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env" 4 | ], 5 | "plugins": [ 6 | ["@babel/plugin-transform-runtime", { 7 | }], 8 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 9 | ["@babel/plugin-transform-react-jsx", { "pragma": "h", "pragmaFrag": "Fragment","throwIfNamespace":false }], 10 | ] 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/contracts/eth_provider.js: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import config from 'config' 3 | 4 | var provider 5 | 6 | if (!provider) { 7 | switch(config.provider.type) { 8 | case 'rpc': 9 | provider = new ethers.providers.JsonRpcProvider(config.provider.uri,config.provider.network) 10 | break 11 | } 12 | } 13 | export default provider 14 | -------------------------------------------------------------------------------- /src/web/main.scss: -------------------------------------------------------------------------------- 1 | @import "node_modules/bulmaswatch/lumen/variables"; 2 | //$body-background-color: #549127; 3 | //$title-color: #fff; 4 | //$text: #fff; 5 | @import "~bulma/bulma"; 6 | @import "node_modules/bulmaswatch/lumen/overrides"; 7 | 8 | .invoice { 9 | font-size: 12px; 10 | } 11 | .invoice .buttons { 12 | margin-top: 1rem; 13 | } 14 | .hero-foot a { 15 | color: #fff; 16 | } 17 | -------------------------------------------------------------------------------- /src/web/routes.js: -------------------------------------------------------------------------------- 1 | import Home from './home' 2 | import Invoice from './invoice' 3 | 4 | export default { 5 | '/': { 6 | OnEnter: (state) => { 7 | return Home.Init({...state, viewFn: Home.View}) 8 | } 9 | }, 10 | '/invoice/:id': { 11 | OnEnter: (state, params) => { 12 | console.log("OnEnter") 13 | return Invoice.Init({...state, viewFn: Invoice.View}, params.id) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tools/scripts.webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const nodeExternals = require('webpack-node-externals') 3 | 4 | module.exports = _ => ({ 5 | target: 'node', 6 | entry: [ 7 | './src/scripts/wallets.js' 8 | ], 9 | output: { 10 | filename: './wallets.js', 11 | path: path.resolve('./dist'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | exclude: /node_modules/, 18 | loader: "babel-loader", 19 | } 20 | ], 21 | }, 22 | externals: nodeExternals(), 23 | }) 24 | 25 | -------------------------------------------------------------------------------- /src/common/validators.js: -------------------------------------------------------------------------------- 1 | 2 | export function validateInvoiceForm(form) { 3 | const errors = [] 4 | if (!form.items || !form.items.length) { 5 | errors.push('items are missing') 6 | return errors 7 | } 8 | let total = 0 9 | for(let item of form.items) { 10 | if (!item.description) { 11 | errors.push('item description can\'t be empty') 12 | break 13 | } 14 | total += item.amount * (item.quantity || 1) 15 | } 16 | if (!total) 17 | errors.push('invoice total') 18 | form.totalAmount = total 19 | form.paidAmount = 0 20 | return errors 21 | } 22 | -------------------------------------------------------------------------------- /config/default.cson: -------------------------------------------------------------------------------- 1 | listen: 8000 2 | provider: 3 | type: "rpc" 4 | uri: "http://localhost:8545" 5 | #uri: "https://mainnet.vnode.app/v1/" 6 | 7 | apiKey: "CHANGEME" 8 | 9 | invoices: 10 | minConfirmations: 1 11 | 12 | public: 13 | allowTokens: ['DAI'] 14 | contracts: 15 | DAI: 16 | address: "0x6b175474e89094c44da98b954eedeac495271d0f" # Mainnet 17 | # address: "0xC4375B7De8af5a38a93548eb8453a498222C4fF2" DAI token on Kovan network 18 | USDT: 19 | address: "0xdAC17F958D2ee523a2206206994597C13D831ec7" 20 | USDC: 21 | address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" 22 | TUSD: 23 | address: "0x0000000000085d4780B73119b644AE5ecd22b376" 24 | -------------------------------------------------------------------------------- /src/web/index.js: -------------------------------------------------------------------------------- 1 | import { app } from 'hyperapp' 2 | import { interval } from '@hyperapp/time' 3 | import withRouter from '@mrbarrysoftware/hyperapp-router' 4 | import Main from './main' 5 | import routes from './routes' 6 | import { FetchInvoice, FetchInvoiceFx } from './invoice' 7 | 8 | const Init = [ 9 | {} 10 | ] 11 | 12 | const container = document.getElementById('app') 13 | 14 | withRouter(app)({ 15 | router: { 16 | RouteAction: (state, {params, path}) => ({...state}), 17 | disableAnchorCapture: false, 18 | routes, 19 | }, 20 | init: Init, 21 | node: container, 22 | view: Main, 23 | subscriptions: state => { 24 | const subs = [] 25 | subs.push(state.invoice ? interval([FetchInvoice,state.invoice._id], {delay:5000}) : false ) 26 | console.log("SUBS",subs) 27 | return subs 28 | } 29 | }) 30 | 31 | -------------------------------------------------------------------------------- /src/web/main.js: -------------------------------------------------------------------------------- 1 | import { h } from 'hyperapp' 2 | import Invoice from './invoice' 3 | import './main.scss' 4 | 5 | 6 | export default state => { 7 | if (__DEV__) 8 | console.log('===== STATE ======',state) 9 | return
10 |
11 |
12 |
13 |
14 | { state.viewFn ? state.viewFn(state) : 'Loading' } 15 |
16 |
17 |
18 |
19 |
20 |

21 | Powered by Decentrapay 22 |

23 |
24 |
25 |
26 |
27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/server/jobs/callbacks.js: -------------------------------------------------------------------------------- 1 | import request from 'request' 2 | import Invoices from '../invoice/manager' 3 | import config from 'config' 4 | 5 | export async function checkPendingNotifications() { 6 | const invoices = await Invoices.findPaidInvoices() 7 | for(let invoice of invoices) { 8 | if (!invoice.notified && invoice.callbacks) { 9 | request.post(invoice.callbacks.paid.url, {json:{token:invoice.callbacks.token,invoiceId:invoice._id,metadata:invoice.metadata}}, (err, resp) => { 10 | if (err || resp.statusCode != 200) { 11 | console.log(`invoice #${invoice._id}: callback error occured:`,err) 12 | return 13 | } 14 | Promise.resolve(Invoices.updateInvoice(invoice._id,{notified:Date.now(),state:'closed'})) 15 | }) 16 | } 17 | } 18 | } 19 | 20 | export function runCallbackJob() { 21 | checkPendingNotifications() 22 | setInterval(checkPendingNotifications, 15000) 23 | } 24 | -------------------------------------------------------------------------------- /src/server/api_v1.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router' 2 | import Invoices from './invoice/manager' 3 | import { validateInvoiceForm } from '../common/validators' 4 | import config from 'config' 5 | 6 | const router = new Router() 7 | 8 | router.get('/version', (ctx,next) => { 9 | ctx.body = 'v1' 10 | }) 11 | 12 | router.post('/invoice', async (ctx, next) => { 13 | const form = ctx.request.body 14 | if (config.apiKey && (config.apiKey !== form.apiKey) ) 15 | ctx.throw(403, 'invalid token') 16 | const errors = validateInvoiceForm(form) 17 | if (errors.length) { 18 | ctx.body = { 19 | errors 20 | } 21 | return 22 | } 23 | form.expires = form.expires || Date.now() + 24 * 60 * 60000 24 | const invoice = await Invoices.createInvoice(form) 25 | ctx.body = { 26 | invoiceId: invoice._id 27 | } 28 | }) 29 | 30 | router.get('/invoice/:id', async (ctx, next) => { 31 | const invoice = await Invoices.getInvoice(ctx.params.id) 32 | console.log("INVOICE",invoice) 33 | ctx.body = invoice 34 | }) 35 | 36 | export default router 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 DaiPay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import Koa from 'koa' 4 | import koaLogger from 'koa-logger' 5 | import Router from 'koa-router' 6 | import bodyParser from 'koa-bodyparser' 7 | import mount from 'koa-mount' 8 | import api from './api_v1' 9 | 10 | const router = new Router() 11 | const app = new Koa() 12 | 13 | if (__DEV__ || __TEST__) 14 | app.use(koaLogger()) 15 | app 16 | .use(bodyParser()) 17 | .use(router.routes()) 18 | .use(mount('/api/v1',api.routes())) 19 | 20 | if (__DEV__) { 21 | const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require; 22 | const koaWebpack = require('koa-webpack') 23 | const serve = require('koa-static') 24 | const [webConfig, ] = requireFunc('../tools/app.webpack.config.js')() 25 | koaWebpack({ 26 | config: webConfig, 27 | devMiddleware: {index:false} 28 | }).then(middleware => { 29 | app.use(middleware) 30 | app.use(serve('./static')) 31 | app.use(async (ctx, next) => { 32 | if (ctx.request.method === 'POST') { 33 | return next() 34 | } 35 | const filename = path.resolve(webConfig.output.path, 'index.html') 36 | const index = fs.readFileSync(filename,{encoding:'utf8'}) 37 | ctx.response.type = 'html' 38 | ctx.response.body = index 39 | }) 40 | }) 41 | } 42 | export default app 43 | -------------------------------------------------------------------------------- /src/contracts/abi/ERC20.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"guy","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"},{"name":"guy","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"}] 2 | -------------------------------------------------------------------------------- /src/server/invoice/manager.js: -------------------------------------------------------------------------------- 1 | import PouchDB from 'pouchdb' 2 | import { ethers } from 'ethers' 3 | PouchDB.plugin(require('pouchdb-find')) 4 | PouchDB.plugin(require('pouchdb-adapter-node-websql')) 5 | 6 | let walletDB, invoiceDB 7 | 8 | if (!walletDB) { 9 | walletDB = new PouchDB('wallets.db', {adapter:'websql'}) 10 | invoiceDB = new PouchDB('invoices.db', {adapter:'websql'}) 11 | invoiceDB.createIndex({ 12 | index: { 13 | fields: ['state'] 14 | } 15 | }) 16 | 17 | } 18 | 19 | export default class { 20 | static async createInvoice(form) { 21 | delete form['apiKey'] 22 | form.currency = form.currency || 'DAI' 23 | const props = {...form} 24 | props.created = Date.now() 25 | let wallet = ethers.Wallet.createRandom() 26 | wallet = {address:wallet.address,privateKey:wallet.privateKey} 27 | let result = await walletDB.post(wallet) 28 | props.wallet = { 29 | _id: result.id, 30 | address: wallet.address, 31 | key: wallet.privateKey, 32 | created: Date.now() 33 | } 34 | props.state = 'pending' 35 | result = await invoiceDB.post(props) 36 | return invoiceDB.get(result.id).then( res => { console.log(res); return res } ) 37 | } 38 | static getInvoice(id) { 39 | return invoiceDB.get(id) 40 | } 41 | static findPendingInvoices() { 42 | return invoiceDB.find({selector:{state:{$in:['pending','confirming']}}}).then( result => result.docs ) 43 | } 44 | static findPaidInvoices() { 45 | return invoiceDB.find({selector:{state:'paid'}}).then( result => result.docs ) 46 | } 47 | static async updateInvoice(id, upd) { 48 | let doc = await invoiceDB.get(id) 49 | doc = Object.assign(doc, upd) 50 | return invoiceDB.put({_id:id,_rev:doc._rev,...doc}) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/server/jobs/invoices.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { DAI, ethProvider } from '../../contracts' 3 | import Invoices from '../invoice/manager' 4 | import { fromBN } from '../../common/utils' 5 | import config from 'config' 6 | 7 | export async function checkPendingInvoices(tokenContract, provider) { 8 | const invoices = await Invoices.findPendingInvoices() 9 | console.log("INVOICES",invoices) 10 | const curBlock = await provider.getBlockNumber() 11 | const tasks = [] 12 | console.log(`checkPendingInvoices: found ${invoices.length} invoices`) 13 | for(let invoice of invoices) { 14 | console.log(invoice) 15 | const balanceBN = (await tokenContract.balanceOf(invoice.wallet.address)).toString() 16 | const balance = fromBN(balanceBN) 17 | const upd = {} 18 | if (balance != invoice.paidAmount) { 19 | upd.paidAmount = balance 20 | } 21 | const expires = invoice.expires || (invoice.created + 24 * 60 * 60000) // 1 day 22 | if (Date.now() > expires) { 23 | upd.state = 'expired' 24 | } 25 | if (balance >= invoice.totalAmount) { 26 | if (config.invoices.minConfirmations) { 27 | if (invoice.confirmBlock) { 28 | console.log('curBlock',curBlock,invoice.confirmBlock) 29 | if (curBlock >= invoice.confirmBlock) { 30 | upd.state = 'paid' 31 | } 32 | } else { 33 | upd.state = 'confirming' 34 | upd.confirmBlock = curBlock + config.invoices.minConfirmations 35 | } 36 | } else 37 | upd.state = 'paid' 38 | } 39 | if (!_.isEmpty(upd)) { 40 | console.log(`updating invoice #${invoice._id}`,upd) 41 | tasks.push(Invoices.updateInvoice(invoice._id, upd)) 42 | } 43 | } 44 | return tasks 45 | } 46 | export function runLastBlockJob() { 47 | let curBlock, lastBlock = 0 48 | const updateLastBlock = async _ => { 49 | try { 50 | curBlock = await ethProvider.getBlockNumber() 51 | if (curBlock > lastBlock) { 52 | lastBlock = ethProvider.blockNumber 53 | await checkPendingInvoices(DAI, ethProvider) 54 | } 55 | } catch(e) { 56 | console.log(e) 57 | } 58 | setTimeout(updateLastBlock, 1000) 59 | } 60 | updateLastBlock() 61 | } 62 | -------------------------------------------------------------------------------- /test/ERC20_ABI.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balances","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowed","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_initialAmount","type":"uint256"},{"name":"_tokenName","type":"string"},{"name":"_decimalUnits","type":"uint8"},{"name":"_tokenSymbol","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event"}] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decentrapay", 3 | "version": "1.0.1", 4 | "description": "Stablecoin payment processor", 5 | "scripts": { 6 | "test": "mocha --compilers js:@babel/register --timeout 20000 test/test.js", 7 | "wallets": "./node_modules/.bin/babel-node ./src/scripts/wallets.js", 8 | "watch": "webpack --config ./tools/app.webpack.config.js --mode development --watch", 9 | "devserver": "$(npm bin)/nodemon -w ./dist ./dist/server", 10 | "dev": "webpack --config ./tools/app.webpack.config.js --mode development && npm-run-all --parallel watch devserver", 11 | "build": "NODE_ENV=production webpack --config ./tools/app.webpack.config.js --mode production --verbose", 12 | "build-scripts": "NODE_ENV=production ./node_modules/.bin/webpack --config ./tools/scripts.webpack.config.js", 13 | "start": "NODE_ENV=production node ./dist/server" 14 | }, 15 | "author": "", 16 | "license": "mit", 17 | "devDependencies": { 18 | "@babel/cli": "^7.10.4", 19 | "@babel/core": "^7.10.4", 20 | "@babel/node": "^7.10.4", 21 | "@babel/plugin-proposal-decorators": "^7.10.4", 22 | "@babel/plugin-transform-react-jsx": "^7.10.4", 23 | "@babel/plugin-transform-runtime": "^7.10.4", 24 | "@babel/preset-env": "^7.10.4", 25 | "@babel/register": "^7.10.4", 26 | "babel-loader": "^8.1.0", 27 | "html-webpack-plugin": "^3.2.0", 28 | "html-webpack-template": "^6.2.0", 29 | "koa-static": "^5.0.0", 30 | "koa-webpack": "^5.3.0", 31 | "mini-css-extract-plugin": "^0.5.0", 32 | "mkdirp": "^0.5.5", 33 | "mocha": "^5.2.0", 34 | "node-sass": "^4.14.1", 35 | "nodemon": "^1.19.4", 36 | "npm-run-all": "^4.1.5", 37 | "sass-loader": "^9.0.1", 38 | "solc": "^0.5.17", 39 | "supertest": "^3.4.2", 40 | "webpack": "^4.43.0", 41 | "webpack-cli": "^3.3.12", 42 | "webpack-node-externals": "^1.7.2" 43 | }, 44 | "dependencies": { 45 | "@babel/runtime": "^7.10.4", 46 | "@hyperapp/router": "^0.7.1", 47 | "@hyperapp/time": "0.0.10", 48 | "@mrbarrysoftware/hyperapp-router": "0.0.1-beta.8", 49 | "bignumber.js": "^8.0.1", 50 | "bulma": "^0.9.0", 51 | "bulmaswatch": "^0.8.1", 52 | "command-line-args": "^5.0.2", 53 | "config": "^3.3.1", 54 | "cson": "^5.1.0", 55 | "css-loader": "^2.1.0", 56 | "ethers": "^4.0.47", 57 | "gasless": "^1.0.3", 58 | "hyperapp": "^2.0.4", 59 | "koa": "^2.13.0", 60 | "koa-bodyparser": "^4.3.0", 61 | "koa-logger": "^3.2.1", 62 | "koa-mount": "^4.0.0", 63 | "koa-router": "^9.1.0", 64 | "mongoose": "^5.9.21", 65 | "pouchdb": "^7.2.1", 66 | "pouchdb-adapter-node-websql": "^7.0.0", 67 | "pouchdb-find": "^7.2.1", 68 | "qrcode": "^1.3.3", 69 | "request": "^2.88.2", 70 | "shortid": "^2.2.14", 71 | "web3": "^1.2.11", 72 | "xhr2": "^0.2.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/scripts/wallets.js: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js' 2 | import commandLineArgs from 'command-line-args' 3 | import { ethers } from 'ethers' 4 | import Invoices from '../server/invoice/manager' 5 | import { DAI,ethProvider } from '../contracts' 6 | import { fromBN,toBN } from '../common/utils' 7 | import xhr2 from 'xhr2' 8 | global.XMLHttpRequest = xhr2 9 | import Gasless from 'gasless' 10 | import Web3 from "web3" 11 | 12 | let web3 = new Web3( 13 | // Replace YOUR-PROJECT-ID with a Project ID from your Infura Dashboard 14 | new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws/v3/8373ce611754454884132be22b562e45") 15 | ); 16 | 17 | 18 | const optionDefs = [ 19 | {name: 'destination', alias:'d', type:String}, 20 | {name: 'gaslimit', type:Number, defaultValue:50000}, 21 | ] 22 | 23 | const fatal = (msg) => { 24 | console.log(msg) 25 | process.exit(1) 26 | } 27 | async function main() { 28 | const options = commandLineArgs(optionDefs) 29 | const invoices = await Invoices.findPaidInvoices() 30 | console.log(`Found ${invoices.length} paid invoices`) 31 | const gasless = new Gasless(web3.currentProvider); 32 | for(let invoice of invoices) { 33 | const wallet = invoice.wallet 34 | let balanceBN = (await DAI.balanceOf(wallet.address)) 35 | let balance = fromBN(balanceBN) 36 | console.log(`${wallet.address} DAI balance: ${balance} `) 37 | if (balance > 0) { 38 | let ethBalance = new BigNumber((await ethProvider.getBalance(wallet.address)).toString()) 39 | console.log(`${wallet.address} ETH balance: ${fromBN(ethBalance)}`) 40 | if (options.destination) { 41 | const account = web3.eth.accounts.wallet.add(wallet.key) 42 | const gasPrice = await web3.eth.getGasPrice() 43 | const daiFee = await gasless.getFee(gasPrice) 44 | console.log(daiFee) 45 | let from = wallet.address 46 | let to = options.destination 47 | const tx = await gasless.send( 48 | from, 49 | to, 50 | balanceBN, 51 | daiFee, 52 | gasPrice 53 | ) 54 | /* 55 | const gasPrice = new BigNumber((await ethProvider.getGasPrice()).toString()).multipliedBy(1.25) // increase last block gasprice 25% for faster transactions 56 | console.log(`setting gas price to ${gasPrice.toNumber()}`) 57 | 58 | console.log(`collecting ${balance} DAI from ${wallet.address}`) 59 | const DAIWithSigner = DAI.connect(new ethers.Wallet(wallet.privateKey, ethProvider)) 60 | tx = await DAIWithSigner.transfer(options.collect, balanceBN) 61 | try { 62 | await tx.wait() 63 | console.log('done.') 64 | } catch(e) { 65 | console.log('sending tokens failed',e) 66 | } 67 | */ 68 | } 69 | } 70 | } 71 | process.exit(0) 72 | } 73 | 74 | main() 75 | -------------------------------------------------------------------------------- /src/web/invoice.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { h } from 'hyperapp' 3 | import { classNames, displayCurrency } from './helpers' 4 | import QRCode from 'qrcode' 5 | import config from 'config' 6 | 7 | export function fx(a) { 8 | return function(b) { 9 | return [a, b] 10 | } 11 | } 12 | function SetInvoice(state, invoice) { 13 | return {...state, invoice} 14 | } 15 | 16 | function SetQRCode(state, qrcode) { 17 | return {...state, qrcode} 18 | } 19 | 20 | async function fetchInvoice(dispatch, params) { 21 | assert(params.id) 22 | const opts = {} 23 | opts.headers = new Headers({'Content-Type': 'application/json'}) 24 | let result = await fetch(`/api/v1/invoice/${params.id}`, opts) 25 | const invoice = await result.json() 26 | dispatch([SetInvoice, invoice]) 27 | if (params.generateQR) { 28 | let qrcode = await QRCode.toDataURL(`ethereum:${invoice.wallet.address}/transfer?address=${config.contracts.DAI.address}&uint256=${invoice.totalAmount}`) 29 | dispatch([SetQRCode, qrcode]) 30 | } 31 | } 32 | 33 | export const FetchInvoiceFx = fx(fetchInvoice) 34 | 35 | export function FetchInvoice(state, id) { 36 | return [ 37 | state, 38 | FetchInvoiceFx({id}) 39 | ] 40 | } 41 | 42 | function showStatusLine(state) { 43 | let cls, msg 44 | switch(state) { 45 | case 'pending': 46 | cls = ['has-background-info','has-text-white'] 47 | msg =