├── test ├── test_contract │ └── contract.js ├── models │ ├── contract.js │ ├── token.js │ ├── ledger.js │ ├── credit.js │ ├── balance.js │ └── debit.js ├── server.js ├── genkey.js ├── http │ ├── host_meta.js │ ├── billing_service.js │ └── compute_service.js ├── application.js ├── amortizer.js ├── billing_service.js └── compute_service.js ├── nodemon.json ├── lib ├── events.js ├── features.js ├── overdraft.js ├── bitcoin.js ├── formatter.js ├── log.js ├── db.js ├── application.js ├── token.js ├── engine.js ├── index.js ├── billing_service.js ├── contract_storage.js ├── server.js ├── config.js ├── amortizer.js └── compute_service.js ├── routes ├── root.js ├── host_meta.js ├── get_health.js ├── get_token_metadata.js ├── post_token.js └── post_contract.js ├── knexfile.js ├── deploy-to-testnet.sh ├── fig.yml ├── processes ├── billing_service.js ├── server.js ├── amortization.js ├── compute_service.js ├── billing_bitcoind.js └── ripple_billing.js ├── compute-service ├── server.js ├── config │ └── routes.js └── controllers │ ├── quotes.js │ └── instances.js ├── migrations ├── 20150210133058_create-ledgers.js ├── 20141223232043_balances_belong_to_tokens.js ├── 20150114152243_add_credits.js ├── 20150114152246_add_debits.js ├── 20150212120755_create_addresses.js ├── 20140802170330_add_contracts.js ├── 20140802170344_add_balances.js ├── 20141023163216_add_contract_storage_entries.js └── 20140802170601_add_tokens.js ├── billing-service ├── server.js ├── config │ └── routes.js └── controllers │ ├── balances.js │ ├── credits.js │ └── debits.js ├── .ebextensions ├── port.config ├── libseccomp.config └── ssl.config ├── DEPLOY.md ├── .travis.yml ├── Dockerfile ├── .gitignore ├── test-contract-upload.sh ├── LICENSE ├── bin └── codius-host ├── models ├── debit.js ├── credit.js ├── contract_storage_entry.js ├── address.js ├── contract.js ├── ledger.js ├── token.js └── balance.js ├── app.js ├── circle.yml ├── package.json └── README.md /test/test_contract/contract.js: -------------------------------------------------------------------------------- 1 | console.log('Hello world!'); -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["node_modules/codius-engine/contract_filesystem/*"] 3 | } 4 | -------------------------------------------------------------------------------- /lib/events.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | 3 | module.exports = new EventEmitter(); 4 | 5 | -------------------------------------------------------------------------------- /routes/root.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(req, res) { 3 | res.redirect(301, '/.well-known/host-meta.json'); 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | var nconf = require('./lib/config'); 2 | 3 | var env = process.env.NODE_ENV || 'development'; 4 | exports[env] = nconf.get('db'); 5 | 6 | -------------------------------------------------------------------------------- /deploy-to-testnet.sh: -------------------------------------------------------------------------------- 1 | git clone git@github.com:codius/rippled-skynet skynet 2 | cd skynet 3 | git submodule update --init 4 | chmod a-rwx,u+r keys/* 5 | ansible-playbook -i ec2.py -e codius_version=$CIRCLE_SHA1 codius-host-docker.yml 6 | -------------------------------------------------------------------------------- /fig.yml: -------------------------------------------------------------------------------- 1 | web: 2 | build: . 3 | command: node_modules/.bin/nodemon app.js 4 | ports: 5 | - 2633:2633 6 | volumes: 7 | - .:/code 8 | links: 9 | - db 10 | environment: 11 | NODE_ENV: fig 12 | db: 13 | image: orchardup/postgresql 14 | ports: 15 | - 5432 16 | -------------------------------------------------------------------------------- /lib/features.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var flipper = require('flipper'); 3 | var featuresJsonFilePath = __dirname+'/../config/features.json'; 4 | 5 | if (fs.existsSync(featuresJsonFilePath)) { 6 | flipper.persist(featuresJsonFilePath); 7 | } 8 | 9 | module.exports = flipper; 10 | 11 | -------------------------------------------------------------------------------- /processes/billing_service.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(codius) { 3 | 4 | var server = require(__dirname+'/../billing-service/server')(codius) 5 | var port = (parseInt(process.env.PORT) || 5000) + 15 6 | 7 | server.listen(port, function() { 8 | codius.logger.info('Codius Billing Service listening on port', port) 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /compute-service/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var BridgesExpress = require('bridges-express') 3 | 4 | module.exports = function(codius) { 5 | 6 | var server = new BridgesExpress({ 7 | directory: path.join(__dirname), 8 | controllers: { 9 | inject: [codius] 10 | } 11 | }) 12 | 13 | return server 14 | } 15 | -------------------------------------------------------------------------------- /migrations/20150210133058_create-ledgers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.up = function(knex, Promise) { 4 | return knex.schema.createTable('ledgers', function(t) { 5 | t.increments().primary(); 6 | t.string('name').notNull(); 7 | t.string('last_hash'); 8 | }); 9 | }; 10 | 11 | exports.down = function(knex, Promise) { 12 | return knex.schema.dropTable('ledgers') 13 | }; 14 | -------------------------------------------------------------------------------- /lib/overdraft.js: -------------------------------------------------------------------------------- 1 | 2 | function Overdraft(remainder, debit) { 3 | Error.captureStackTrace(this, Overdraft); 4 | 5 | this.debit = debit; 6 | this.remainder = remainder; 7 | this.message = 'overdraft'; 8 | } 9 | 10 | // Inherit from Error // 11 | Overdraft.prototype = Object.create(Error.prototype); 12 | Overdraft.prototype.constructor = Overdraft; 13 | 14 | module.exports = Overdraft; 15 | -------------------------------------------------------------------------------- /billing-service/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var BridgesExpress = require('bridges-express') 3 | var port = (parseInt(process.env.PORT) || 5000) + 15 4 | 5 | module.exports = function(codius) { 6 | 7 | var server = new BridgesExpress({ 8 | directory: path.join(__dirname), 9 | controllers: { 10 | inject: [codius] 11 | } 12 | }) 13 | 14 | return server 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /migrations/20141223232043_balances_belong_to_tokens.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.up = function(knex, Promise) { 4 | return knex.schema.table('balances', function(t){ 5 | t.integer('token_id').unsigned().index().references('id').inTable('tokens'); 6 | }); 7 | }; 8 | 9 | exports.down = function(knex, Promise) { 10 | return knex.schema.table('balances', function(t){ 11 | t.dropColumn('token_id'); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /migrations/20150114152243_add_credits.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.up = function(knex, Promise) { 4 | return knex.schema.createTable('credits', function(t) { 5 | t.increments().primary(); 6 | t.integer('balance_id').unsigned().index().references('id').inTable('balances'); 7 | t.decimal('amount'); 8 | }); 9 | }; 10 | 11 | exports.down = function(knex, Promise) { 12 | return knex.schema.dropTable('credits') 13 | }; 14 | -------------------------------------------------------------------------------- /.ebextensions/port.config: -------------------------------------------------------------------------------- 1 | container_commands: 2 | 01-iptables-flush: 3 | command: iptables -F -t nat 4 | env: 5 | PATH: /usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin 6 | 02-iptables-redirect-https: 7 | command: iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8080 8 | env: 9 | PATH: /usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin 10 | -------------------------------------------------------------------------------- /DEPLOY.md: -------------------------------------------------------------------------------- 1 | Deployment steps: 2 | 3 | * Push branch to github 4 | * Create pull request against master, with "+r" in the body 5 | * Wait for at least one LGTM/:+1: 6 | * Monty merges 7 | * Circleci builds new merge 8 | * Docker image is built and pushed to docker.io hub 9 | * Circle checks out skynet repository and skynet-keys private repo 10 | * Circle runs codius-host-docker.yml playbook to deploy new docker image on 11 | codius AWS account 12 | -------------------------------------------------------------------------------- /compute-service/config/routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(router, controllers) { 2 | 3 | router.post('/instances', controllers.instances.create) 4 | router.get('/instances', controllers.instances.index) 5 | router.get('/instances/:token', controllers.instances.show) 6 | router.delete('/instances/:token', controllers.instances.stop) 7 | 8 | // TODO: Add quotes controller 9 | // router.post('/quotes', controllers.quotes.create) 10 | } -------------------------------------------------------------------------------- /migrations/20150114152246_add_debits.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.up = function(knex, Promise) { 4 | return knex.schema.createTable('debits', function(t) { 5 | t.increments().primary(); 6 | t.integer('balance_id').unsigned().index().references('id').inTable('balances'); 7 | t.decimal('amount'); 8 | }); 9 | }; 10 | 11 | exports.down = function(knex, Promise) { 12 | return knex.schema.dropTable('debits') 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install -g knex 6 | - knex migrate:latest 7 | - openssl req -x509 -nodes -days 365 -subj '/CN=codius.org' -newkey rsa:2048 -keyout server.key -out server.crt 8 | notifications: 9 | slack: 10 | secure: dWShGhYEiV7l9wyNl+vuPi16f9PmKYqs/hfCmmQBFf+080ViBxyXXCiqTyyvQJ8AkQ7sEL7P2Ke5q/bXzXftdpc2L6q3iJYvMI9nudoAH1Zc19m+LsIMu41pNpe7igJpJgWasodvzoHnj452jikZCd0ioppo9mEfp0e8+3O5eSE= -------------------------------------------------------------------------------- /billing-service/config/routes.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(router, controllers) { 3 | 4 | router.get('/contracts/:token', controllers.balances.show) 5 | router.post('/contracts/:token/credits', controllers.credits.create) 6 | router.post('/contracts/:token/debits', controllers.debits.create) 7 | 8 | router.get('/contracts/:token/credits', controllers.credits.index) 9 | router.get('/contracts/:token/debits', controllers.debits.index) 10 | } 11 | 12 | -------------------------------------------------------------------------------- /migrations/20150212120755_create_addresses.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | exports.up = function(knex, Promise) { 5 | return knex.schema.createTable('addresses', function(t) { 6 | t.increments().primary(); 7 | t.string('address').notNull(); 8 | t.integer('ledger_id').notNull(); 9 | t.integer('token_id').notNull(); 10 | }); 11 | }; 12 | 13 | exports.down = function(knex, Promise) { 14 | return knex.schema.dropTable('addresses') 15 | }; 16 | 17 | 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM codius/codius.org:base 2 | 3 | # Install NPM dependencies 4 | # We do this first so that it can be cached even if the rest of the 5 | # application changes. 6 | ADD package.json /code/package.json 7 | WORKDIR /code 8 | RUN npm install --no-color 9 | 10 | # Add rest of application 11 | ENV PORT 8080 12 | ENV CONTRACTS_STORAGE /contracts/ 13 | EXPOSE 8080 14 | VOLUME ["/contracts"] 15 | 16 | ADD . /code 17 | 18 | ENTRYPOINT ["/usr/local/bin/node", "bin/codius-host"] 19 | CMD ["start"] 20 | -------------------------------------------------------------------------------- /compute-service/controllers/quotes.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird') 2 | 3 | module.exports = function(codius) { 4 | 5 | return { 6 | create: function(req, res, next) { 7 | return compute.getQuote(req.body.manifest).then(function(price) { 8 | res.send({ 9 | success: true, 10 | manifest_hash: req.body.manifest.manifest_hash, 11 | // price: cpus 12 | // interval: milliseconds 13 | // signature: webtoken 14 | }) 15 | }) 16 | .error(next) 17 | }, 18 | } 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Coverage directory used by tools like istanbul 2 | coverage 3 | 4 | # Dependency directory 5 | # Commenting this out is preferred by some people, see 6 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 7 | node_modules 8 | 9 | # Users Environment Variables 10 | .lock-wscript 11 | 12 | # Development database 13 | dev.sqlite3 14 | 15 | # SSL keys 16 | /server.key 17 | /server.crt 18 | .elasticbeanstalk/ 19 | 20 | # Elastic Beanstalk Files 21 | .elasticbeanstalk/* 22 | !.elasticbeanstalk/*.cfg.yml 23 | !.elasticbeanstalk/*.global.yml 24 | -------------------------------------------------------------------------------- /test-contract-upload.sh: -------------------------------------------------------------------------------- 1 | expected="e9f32f730046f6522f52fbab4edb22485bfdd0e2a873e81c65337384c3d1bffe" 2 | cd test/test_contract 3 | CODIUS_UNAUTHORIZED_SSL=true CODIUS_HOST=Http://localhost:8080/ codius upload | tee /tmp/upload-output 4 | token=$(grep 'Application metadata' /tmp/upload-output | awk -F '/' '{print $5}') 5 | curl -k https://localhost:8080/token/${token} | tee /tmp/api-output 6 | hash=$(cat /tmp/api-output | jq .hash -r) 7 | 8 | if [ "${expected}" != "${hash}" ];then 9 | echo "Contract hash mismatch. ${hash} != ${expected}" 10 | exit 1 11 | else 12 | echo "Contract hashes match." 13 | exit 0 14 | fi 15 | -------------------------------------------------------------------------------- /test/models/contract.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var Contract = require(path.join(__dirname, '/../../models/contract')).model; 3 | var assert = require('assert'); 4 | 5 | describe('Contracts Model', function() { 6 | var contract; 7 | 8 | it('should persist a contract to the database', function(done) { 9 | 10 | new Contract().save().then(function(record) { 11 | contract = record; 12 | assert(contract.id); 13 | done(); 14 | }); 15 | }); 16 | 17 | after(function(done) { 18 | contract.destroy().then(function() { 19 | done(); 20 | }); 21 | }); 22 | }); 23 | 24 | -------------------------------------------------------------------------------- /billing-service/controllers/balances.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(codius) { 3 | var billing = new codius.BillingService(); 4 | var Token = codius.Token; 5 | 6 | return { 7 | show: function(req, res, next) { 8 | 9 | new Token({ token: req.params.token }).fetch() 10 | .then(function(token) { 11 | if (token) { 12 | billing.getBalance(token).then(function(balance) { 13 | res.status(200).send({ 14 | success: true, 15 | balance: balance.get('balance') 16 | }) 17 | }) 18 | } else { 19 | next(new Error('token not found')) 20 | } 21 | }) 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /test/models/token.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var assert = require('assert'); 3 | var Token = require(path.join(__dirname+'/../../models/token')).model; 4 | 5 | describe('Token Model', function() { 6 | var token; 7 | 8 | before(function(done) { 9 | new Token().save().then(function(_token) { 10 | token = _token; 11 | done(); 12 | }); 13 | }); 14 | 15 | after(function(done) { 16 | token.destroy().then(function() { 17 | done(); 18 | }); 19 | }); 20 | 21 | it('should be created with a balance of zero', function(done) { 22 | token.getBalance().then(function(balance) { 23 | assert.strictEqual(balance.get('balance'), 0); 24 | done(); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /.ebextensions/libseccomp.config: -------------------------------------------------------------------------------- 1 | sources: 2 | /home/ec2-user: https://s3-us-west-1.amazonaws.com/codius-host-deps/libseccomp-2.1.1.tar.gz 3 | container_commands: 4 | 01-configure: 5 | command: ./configure --prefix=/usr 6 | cwd: /home/ec2-user/libseccomp-2.1.1 7 | env: 8 | PATH: /usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin 9 | 02-make: 10 | command: make 11 | cwd: /home/ec2-user/libseccomp-2.1.1 12 | env: 13 | PATH: /usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin 14 | 03-make-install: 15 | command: make install 16 | cwd: /home/ec2-user/libseccomp-2.1.1 17 | env: 18 | PATH: /usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ripple Labs Inc. 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /lib/bitcoin.js: -------------------------------------------------------------------------------- 1 | var Bitcoin = require('bitcoinjs-lib'); 2 | 3 | var Balance = require('../models/balance').model; 4 | 5 | var config = require('./config'); 6 | 7 | // TODO: store wallets instead of generating them each time 8 | function generateDeterministicWallet(index) { 9 | 10 | if (!config.get('bitcoin_bip32_extended_public_key')) { 11 | throw new Error('No Bitcoin public key supplied'); 12 | } 13 | 14 | var hdnode = Bitcoin.HDNode.fromBase58(config.get('bitcoin_bip32_extended_public_key')); 15 | return hdnode.derive(index).getAddress().toString(); 16 | } 17 | 18 | function convertSatoshisToComputeUnits(satoshis) { 19 | return satoshis / 100000000 * config.get('compute_units_per_bitcoin'); 20 | } 21 | exports.generateDeterministicWallet = generateDeterministicWallet; 22 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var bluebird = require('bluebird'); 2 | 3 | bluebird.Promise.longStackTraces(); 4 | 5 | var sinon = require('sinon'); 6 | var assert = require('assert'); 7 | var path = require('path'); 8 | var tls = require('tls'); 9 | var CodiusHost = require(path.join(__dirname, '/../')); 10 | var chai = require('chai'); 11 | var chaiAsPromised = require('chai-as-promised'); 12 | var genkey = require('./genkey'); 13 | var nconf = require('../lib/config'); 14 | 15 | var expect = chai.expect; 16 | 17 | describe('Codius Host primary server', function() { 18 | 19 | beforeEach(function() { 20 | return genkey().then(function(v) { 21 | nconf.set('ssl:key', v.keyPath); 22 | nconf.set('ssl:cert', v.certPath); 23 | }); 24 | }); 25 | 26 | it('#start should bind to port with a tls server', function() { 27 | 28 | var tlsCreateServer = sinon.spy(tls, 'createServer'); 29 | 30 | return new CodiusHost.Server().start().then(function(status) { 31 | assert(tlsCreateServer.called); 32 | }); 33 | }); 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /bin/codius-host: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var program = require('commander'); 4 | var features = require(__dirname+'/../lib/features'); 5 | 6 | function list(val) { 7 | return val.split(','); 8 | } 9 | 10 | program 11 | .on('--help', function() { 12 | console.log('Environment variables:'); 13 | console.log('\tSSL_CERT\t\t- SSL certificate to use'); 14 | console.log('\tSSL_KEY\t\t\t- SSL key to use'); 15 | console.log('\tCONTRACTS_STORAGE\t- Directory to store contracts in'); 16 | }); 17 | 18 | program 19 | .option('-f, --features ', 'List of features', list) 20 | 21 | program 22 | .command('start') 23 | .description('start Codius Host') 24 | .action(function() { 25 | 26 | if (program.features && program.features.length > 0) { 27 | console.log('Features Enabled:'); 28 | program.features.forEach(function(feature) { 29 | console.log('-', feature); 30 | features.enable(feature.toUpperCase()); 31 | }); 32 | } 33 | 34 | require(__dirname+'/../app'); 35 | }); 36 | 37 | program.parse(process.argv); 38 | -------------------------------------------------------------------------------- /models/debit.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var bookshelf = require(path.join(__dirname, '/../lib/db')).bookshelf; 3 | var Promise = require('bluebird'); 4 | 5 | var Debit = bookshelf.Model.extend({ 6 | tableName: 'debits', 7 | initialize: function() { 8 | this.on('creating', this.validate); 9 | this.on('created', this.debitBalance); 10 | }, 11 | validate: function() { 12 | if (!(this.get('amount') > 0)) { 13 | throw new Error('amount must be greater than zero'); 14 | } 15 | }, 16 | updateBalance: function() { 17 | return bookshelf.knex('balances') 18 | .where('id', '=', this.get('balance_id')) 19 | .decrement('balance', this.get('amount')) 20 | } 21 | }); 22 | 23 | Debit.debitBalance = function(balance, amount) { 24 | var debit; 25 | return new Debit({ 26 | amount: amount, 27 | balance_id: balance.get('id') 28 | }) 29 | .save() 30 | .then(function(record) { 31 | debit = record; 32 | return debit.updateBalance() 33 | }) 34 | .then(function() { 35 | return Promise.resolve(debit); 36 | }) 37 | } 38 | 39 | exports.model = Debit; 40 | 41 | -------------------------------------------------------------------------------- /test/models/ledger.js: -------------------------------------------------------------------------------- 1 | var Ledger = require(__dirname+'/../../models/ledger').model; 2 | var Token = require(__dirname+'/../../models/token').model; 3 | var assert = require('assert'); 4 | 5 | describe('Ledger Model', function() { 6 | var ledger, token; 7 | 8 | before(function(done) { 9 | Ledger.findOrCreate({ name: 'bitcoin' }).then(function(record) { 10 | ledger = record; 11 | new Token().save().then(function(record) { 12 | token = record; 13 | done(); 14 | }) 15 | }) 16 | }); 17 | 18 | it('should register an address in the ledger', function(done) { 19 | ledger.registerAddress(token, 'someAddress') 20 | .then(function(address) { 21 | assert.strictEqual(address.get('address'), 'someAddress') 22 | 23 | address.related('ledger').fetch().then(function(related) { 24 | assert.strictEqual(related.get('id'), ledger.get('id')) 25 | done() 26 | }) 27 | }) 28 | }); 29 | 30 | after(function(done) { 31 | ledger.destroy().then(function() { 32 | token.destroy().then(function() { 33 | done(); 34 | }) 35 | }); 36 | }); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /.ebextensions/ssl.config: -------------------------------------------------------------------------------- 1 | Resources: 2 | sslSecurityGroupIngress: 3 | Type: AWS::EC2::SecurityGroupIngress 4 | Properties: 5 | GroupName: {Ref : AWSEBSecurityGroup} 6 | IpProtocol: tcp 7 | ToPort: 443 8 | FromPort: 443 9 | CidrIp: 0.0.0.0/0 10 | container_commands: 11 | 01-download-s3curl: 12 | command: curl -o /usr/local/bin/s3curl.pl https://s3-us-west-1.amazonaws.com/codius-host-ssl-cert/s3curl.pl 13 | 02-make-s3curl-executable: 14 | command: chmod +x /usr/local/bin/s3curl.pl 15 | 03-download-cert: 16 | command: s3curl.pl --id=${AWS_ACCESS_KEY_ID} --key=${AWS_SECRET_KEY} -- -o /tmp/deployment/application/server.crt https://s3-us-west-1.amazonaws.com/codius-host-ssl-cert/star.codius.host.crt 17 | 04-download-key: 18 | command: s3curl.pl --id=${AWS_ACCESS_KEY_ID} --key=${AWS_SECRET_KEY} -- -o /tmp/deployment/application/server.key https://s3-us-west-1.amazonaws.com/codius-host-ssl-cert/star.codius.host.key 19 | 05-download-ca-certs: 20 | command: s3curl.pl --id=${AWS_ACCESS_KEY_ID} --key=${AWS_SECRET_KEY} -- -o /tmp/deployment/application/ca.crt https://s3-us-west-1.amazonaws.com/codius-host-ssl-cert/geotrust.ca.crt 21 | -------------------------------------------------------------------------------- /test/genkey.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird').Promise; 2 | 3 | Promise.longStackTraces(); 4 | 5 | var spawn = require('child_process').spawn; 6 | var temp = require('temp'); 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | 10 | Promise.promisifyAll(fs); 11 | 12 | function genkey() { 13 | return Promise.promisify(temp.mkdir)('codius-host-test').then(function(dir) { 14 | var keyPath = path.join(dir, 'key.pem'); 15 | var certPath = path.join(dir, 'cert.pem'); 16 | return new Promise(function(resolve, reject) { 17 | var sslgen = spawn('openssl', [ 18 | 'req', '-x509', '-nodes', '-days', '365', 19 | '-subj', '/CN=codius.org', '-newkey', 'rsa:2048', '-out', certPath, '-keyout', keyPath]); 20 | 21 | sslgen.on('close', function(code) { 22 | if (code == 0) { 23 | fs.readFile(certPath, function(err, v) { 24 | if (err) return reject(err); 25 | resolve({certContents: v.toString(), certPath: certPath, keyPath: keyPath}); 26 | }); 27 | } else { 28 | reject(code); 29 | } 30 | }); 31 | }); 32 | }); 33 | } 34 | 35 | module.exports = genkey; 36 | -------------------------------------------------------------------------------- /models/credit.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var bookshelf = require(path.join(__dirname, '/../lib/db')).bookshelf; 3 | var Promise = require('bluebird'); 4 | 5 | var Credit = bookshelf.Model.extend({ 6 | tableName: 'credits', 7 | initialize: function() { 8 | this.on('creating', this.validate); 9 | this.on('created', this.creditBalance); 10 | }, 11 | validate: function() { 12 | if (!(this.get('amount') > 0)) { 13 | throw new Error('amount must be greater than zero'); 14 | } 15 | }, 16 | balance: function () { 17 | return this.belongsTo(Balance); 18 | }, 19 | updateBalance: function() { 20 | return bookshelf.knex('balances') 21 | .where('id', '=', this.get('balance_id')) 22 | .increment('balance', this.get('amount')) 23 | } 24 | }); 25 | 26 | Credit.creditBalance = function(balance, amount) { 27 | var credit; 28 | return new Credit({ 29 | balance_id: balance.get('id'), 30 | amount: amount 31 | }) 32 | .save() 33 | .then(function(record) { 34 | credit = record; 35 | return credit.updateBalance() 36 | }) 37 | .then(function() { 38 | return Promise.resolve(credit); 39 | }) 40 | } 41 | 42 | exports.model = Credit; 43 | 44 | -------------------------------------------------------------------------------- /processes/server.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | module.exports = function(codius) { 21 | 22 | new codius.Server().start(codius.compute); 23 | } 24 | 25 | -------------------------------------------------------------------------------- /routes/host_meta.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var config = require('../lib/config') 4 | var features = require('../lib/features') 5 | 6 | module.exports = function(req, res) { 7 | fs.readFile(path.join(__dirname+'/../package.json'), function(error, packagejson) { 8 | fs.readFile(config.get('ssl:cert'), function(error, certificate) { 9 | 10 | var properties = { 11 | documentation: "https://codius.org/docs/using-codius/getting-started", 12 | version: JSON.parse(packagejson.toString()).version, 13 | billing: [], 14 | public_key: certificate.toString() 15 | } 16 | 17 | if (features.isEnabled('RIPPLE_BILLING')) { 18 | properties.billing.push({ 19 | network: 'ripple', 20 | cpu_per_xrp: parseFloat(config.get('compute_units_per_xrp')) 21 | }) 22 | } 23 | 24 | if (features.isEnabled('BITCOIN_BILLING')) { 25 | properties.billing.push({ 26 | network: 'bitcoin', 27 | cpu_per_bitcoin: config.get('compute_units_per_bitcoin') 28 | }) 29 | } 30 | 31 | res.status(200).send({ 32 | properties: properties 33 | }) 34 | }) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /lib/formatter.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var chalk = require('chalk'); 21 | 22 | exports.hash = function (hash) { 23 | return chalk.blue(hash.substr(0, 8)); 24 | }; 25 | -------------------------------------------------------------------------------- /processes/amortization.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | module.exports = function(codius) { 21 | 22 | codius.amortizer.startPollingRunningInstances(100); 23 | } 24 | 25 | -------------------------------------------------------------------------------- /routes/get_health.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var db = require('../lib/db'); 21 | 22 | module.exports = function (req, res, next) { 23 | db.knex.migrate.currentVersion().then(function (version) { 24 | res.status(200).send('OK'); 25 | }).catch(next); 26 | }; 27 | -------------------------------------------------------------------------------- /processes/compute_service.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(codius) { 3 | 4 | var server = require(__dirname+'/../compute-service/server')(codius) 5 | var port = (parseInt(process.env.PORT) || 5000) + 30 6 | 7 | server.listen(port, function() { 8 | codius.logger.info('Codius Compute Service listening on port', port) 9 | }) 10 | 11 | codius.events.on('contract:created', function(token) { 12 | if (token.get('token') in codius.compute._instances) { 13 | codius.logger.debug('Created token already a compute instance:', token.get('token')); 14 | return; 15 | } 16 | new codius.Contract({id:token.get('contract_id')}).fetch().then(function(contract) { 17 | if (!contract) { 18 | throw new Error('Invalid contract token:', token.get('token')); 19 | } 20 | codius.compute._instances[token.get('token')] = { 21 | state: 'pending', 22 | container_hash: contract.get('hash') 23 | }; 24 | }) 25 | }); 26 | 27 | codius.events.on('balance:credited', function(balance){ 28 | new codius.Token({id: balance.get('token_id')}).fetch().then(function(token) { 29 | if (token.get('token') in codius.compute._instances && 30 | codius.compute._instances[token.get('token')].state==='pending') { 31 | codius.compute.startInstance(token); 32 | } 33 | }) 34 | }); 35 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var BridgesApplication = require('bridges-application'); 21 | var codius = require(__dirname+'/lib'); 22 | 23 | new BridgesApplication({ 24 | directory: __dirname, 25 | processes: { 26 | inject: [codius] 27 | } 28 | }).supervisor.start(); 29 | 30 | -------------------------------------------------------------------------------- /billing-service/controllers/credits.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird') 2 | 3 | module.exports = function(codius) { 4 | 5 | var billing = new codius.BillingService(); 6 | 7 | function getToken(token) { 8 | return new codius.Token({ token: token }).fetch() 9 | .then(function(token) { 10 | if (token) { 11 | return Promise.resolve(token) 12 | } else { 13 | return Promise.reject(new Error('token not found')) 14 | } 15 | }) 16 | } 17 | 18 | return { 19 | create: function(req, res, next) { 20 | getToken(req.params.token).then(function(token) { 21 | billing.credit(token, req.body.amount) 22 | .then(function(credit) { 23 | return token.getBalance().then(function(balance) { 24 | res.send({ 25 | success: true, 26 | balance: balance.get('balance'), 27 | credit: credit 28 | }) 29 | }) 30 | }) 31 | }) 32 | .error(next) 33 | }, 34 | 35 | index: function(req, res, next) { 36 | getToken(req.params.token).then(function(token) { 37 | return billing.getCredits(token).then(function(credits) { 38 | res.status(200).send({ 39 | success: true, 40 | credits: credits 41 | }) 42 | }) 43 | }) 44 | .error(next) 45 | } 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /billing-service/controllers/debits.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird') 2 | 3 | module.exports = function(codius) { 4 | 5 | var billing = new codius.BillingService(); 6 | 7 | function getToken(token) { 8 | return new codius.Token({ token: token }).fetch() 9 | .then(function(token) { 10 | if (token) { 11 | return Promise.resolve(token) 12 | } else { 13 | return Promise.reject(new Error('token not found')) 14 | } 15 | }) 16 | } 17 | 18 | return { 19 | create: function(req, res, next) { 20 | getToken(req.params.token).then(function(token) { 21 | return billing.debit(token, req.body.amount) 22 | .then(function(debit) { 23 | return token.getBalance().then(function(balance) { 24 | res.send({ 25 | success: true, 26 | balance: balance.get('balance'), 27 | debit: debit 28 | }) 29 | }) 30 | }) 31 | }) 32 | .error(next) 33 | }, 34 | 35 | index: function(req, res, next) { 36 | getToken(req.params.token).then(function(token) { 37 | return billing.getDebits(token).then(function(debits) { 38 | res.status(200).send({ 39 | success: true, 40 | debits: debits 41 | }) 42 | }) 43 | }) 44 | .error(next) 45 | } 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /lib/log.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var winston = require('winston'); 21 | 22 | // Put winston into CLI mode (prettier) 23 | winston.cli(); 24 | winston.default.transports.console.level = 'debug'; 25 | 26 | var winstonStream = {write: function (data) { 27 | winston.info(data.replace(/\n$/, '')); 28 | }}; 29 | 30 | exports.winston = winston; 31 | exports.winstonStream = winstonStream; 32 | -------------------------------------------------------------------------------- /migrations/20140802170330_add_contracts.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | 'use strict'; 21 | 22 | exports.up = function(knex, Promise) { 23 | return knex.schema.createTable('contracts', function (t) { 24 | t.increments().primary(); 25 | t.string('hash', 64).unique(); 26 | }); 27 | }; 28 | 29 | exports.down = function(knex, Promise) { 30 | return knex.schema.dropTable('contracts'); 31 | }; 32 | -------------------------------------------------------------------------------- /migrations/20140802170344_add_balances.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | 'use strict'; 21 | 22 | exports.up = function(knex, Promise) { 23 | return knex.schema.createTable('balances', function (t) { 24 | t.increments().primary(); 25 | t.integer('balance').notNullable(); 26 | }); 27 | }; 28 | 29 | exports.down = function(knex, Promise) { 30 | return knex.schema.dropTable('balances'); 31 | }; 32 | -------------------------------------------------------------------------------- /models/contract_storage_entry.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var bookshelf = require('../lib/db').bookshelf; 21 | 22 | var Contract = require('./contract'); 23 | 24 | var ContractStorageEntry = bookshelf.Model.extend({ 25 | tableName: 'contract_storage_entries', 26 | contract: function () { 27 | return this.belongsTo(Contract.model); 28 | } 29 | }); 30 | 31 | exports.model = ContractStorageEntry; 32 | -------------------------------------------------------------------------------- /lib/db.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var path = require('path'); 21 | var nconf = require('./config'); 22 | 23 | var conf = nconf.get('db'); 24 | 25 | conf.migrations = { directory: path.resolve(__dirname, '../migrations') }; 26 | 27 | var knex = require('knex').initialize(conf); 28 | var bookshelf = require('bookshelf').initialize(knex); 29 | 30 | exports.knex = knex; 31 | exports.bookshelf = bookshelf; 32 | exports.conf = conf; 33 | -------------------------------------------------------------------------------- /lib/application.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var morgan = require('morgan'); 4 | var config = require('./config'); 5 | var log = require('./log'); 6 | var db = require('./db'); 7 | var engine = require('./engine'); 8 | var tokenLib = require('./token'); 9 | var Token = require('./../models/token').model; 10 | 11 | var routeGetHealth = require('../routes/get_health'); 12 | var routePostContract = require('../routes/post_contract'); 13 | var routePostToken = require('../routes/post_token'); 14 | var routeGetTokenMetadata = require('../routes/get_token_metadata'); 15 | var routeRoot = require('../routes/root'); 16 | var routeHostMeta = require('../routes/host_meta'); 17 | 18 | function Application() { 19 | 20 | var app = express(); 21 | 22 | app.use(morgan(config.get('log_format'), {stream: log.winstonStream})) 23 | 24 | app.set('config', config); 25 | app.set('knex', db.knex); 26 | app.set('bookshelf', db.bookshelf); 27 | app.set('compiler', engine.compiler); 28 | app.set('fileManager', engine.fileManager); 29 | app.set('engine', engine.engine); 30 | 31 | app.get('/', routeRoot) 32 | app.get('/.well-known/host-meta.json', routeHostMeta) 33 | app.get('/health', routeGetHealth); 34 | app.post('/contract', routePostContract); 35 | app.post('/token', routePostToken); 36 | app.get('/token/:token', routeGetTokenMetadata); 37 | 38 | return app; 39 | }; 40 | 41 | module.exports = Application; 42 | 43 | -------------------------------------------------------------------------------- /models/address.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var bookshelf = require('../lib/db').bookshelf; 21 | var Token = require(__dirname+'/token') 22 | var Ledger = require(__dirname+'/ledger') 23 | 24 | var Address = bookshelf.Model.extend({ 25 | tableName: 'addresses', 26 | 27 | ledger: function() { 28 | return this.belongsTo(Ledger.model) 29 | }, 30 | 31 | token: function() { 32 | return this.belongsTo(Token.model) 33 | } 34 | }); 35 | 36 | exports.model = Address; 37 | 38 | -------------------------------------------------------------------------------- /models/contract.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var bookshelf = require('../lib/db').bookshelf; 21 | 22 | var Token = require('./token'); 23 | var ContractStorageEntry = require('./contract_storage_entry'); 24 | 25 | var Contract = bookshelf.Model.extend({ 26 | tableName: 'contracts', 27 | tokens: function () { 28 | return this.hasMany(Token.model); 29 | }, 30 | contractStorageEntries: function () { 31 | return this.hasMany(ContractStorageEntry.model); 32 | } 33 | }); 34 | 35 | exports.model = Contract; 36 | -------------------------------------------------------------------------------- /test/models/credit.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var Credit = require(path.join(__dirname, '/../../models/credit')).model; 3 | var Token = require(path.join(__dirname, '/../../models/token')).model; 4 | var assert = require('assert'); 5 | 6 | describe('Credit Model', function() { 7 | var token; 8 | 9 | before(function(done) { 10 | new Token().save().then(function(_token) { 11 | token = _token; 12 | done(); 13 | }); 14 | }); 15 | 16 | after(function(done) { 17 | token.destroy().then(function() { 18 | done(); 19 | }); 20 | }); 21 | 22 | it('should add to a balance', function(done) { 23 | 24 | token.getBalance().then(function(balance) { 25 | var initialBalance = balance.get('balance'); 26 | 27 | Credit.creditBalance(balance, 5).then(function() { 28 | balance.refresh().then(function(balance) { 29 | assert.strictEqual(balance.get('balance'), 5); 30 | done(); 31 | }); 32 | }) 33 | }); 34 | }); 35 | 36 | it('should reject with a negative amount', function(done) { 37 | 38 | new Credit({ amount: -5 }).save().error(function(error) { 39 | assert(error); 40 | assert.strictEqual(error.message, 'amount must be greater than zero'); 41 | done(); 42 | }); 43 | }); 44 | 45 | it('should reject with an empty amount', function(done) { 46 | 47 | new Credit().save().error(function(error) { 48 | assert(error); 49 | assert.strictEqual(error.message, 'amount must be greater than zero'); 50 | done(); 51 | }); 52 | }); 53 | }); 54 | 55 | -------------------------------------------------------------------------------- /migrations/20141023163216_add_contract_storage_entries.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | 'use strict'; 21 | 22 | exports.up = function(knex, Promise) { 23 | return knex.schema.createTable('contract_storage_entries', function (t) { 24 | t.increments().primary(); 25 | t.integer('contract_id').unsigned().index().references('id').inTable('contracts'); 26 | t.string('key'); 27 | t.json('value'); 28 | }); 29 | }; 30 | 31 | exports.down = function(knex, Promise) { 32 | return knex.schema.dropTable('contract_storage_entries'); 33 | }; 34 | -------------------------------------------------------------------------------- /test/models/balance.js: -------------------------------------------------------------------------------- 1 | var Balance = require(__dirname+'/../../models/balance').model; var assert = require('assert'); 2 | 3 | describe('Balance Model', function() { 4 | var balance 5 | 6 | before(function(done) { 7 | new Balance().save().then(function(record) { 8 | balance = record; 9 | done(); 10 | }); 11 | }); 12 | 13 | after(function(done) { 14 | balance.destroy().then(function() { 15 | done(); 16 | }); 17 | }); 18 | 19 | it('should have an amount of zero', function() { 20 | assert.strictEqual(balance.get('balance'), 0); 21 | }); 22 | 23 | it('should increase the balance with a credit', function(done) { 24 | 25 | balance.credit(5500).then(function() { 26 | balance.refresh().then(function(balance) { 27 | assert.strictEqual(balance.get('balance'), 5500); 28 | done(); 29 | }) 30 | }); 31 | }); 32 | 33 | it('should return a record of the credit', function(done) { 34 | balance.credit(5500).then(function(credit) { 35 | assert.strictEqual(credit.get('amount'), 5500); 36 | 37 | balance.refresh().then(function(balance) { 38 | assert.strictEqual(balance.get('balance'), 11000); 39 | done(); 40 | }); 41 | }); 42 | }); 43 | 44 | it('should reduce the balance with a debit', function(done) { 45 | balance.debit(100).then(function(debit) { 46 | assert.strictEqual(debit.get('amount'), 100); 47 | 48 | balance.refresh().then(function(balance) { 49 | assert.strictEqual(balance.get('balance'), 10900); 50 | done(); 51 | }); 52 | }); 53 | }); 54 | }); 55 | 56 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | general: 2 | artifacts: 3 | - "coverage/lcov-report" 4 | machine: 5 | services: 6 | - docker 7 | dependencies: 8 | pre: 9 | - sudo apt-get update -qq 10 | - sudo apt-get install -qq multiarch-support software-properties-common 11 | - sudo apt-add-repository -y ppa:ansible/ansible 12 | - sudo apt-get update -qq 13 | - sudo apt-get install -qq libseccomp-dev libseccomp2:i386 jq ansible 14 | - sudo pip install awscli boto 15 | - npm install -g istanbul-coveralls 16 | test: 17 | pre: 18 | - mkdir -p /tmp/contracts/ 19 | - yes US | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/contracts/server.key -out /tmp/contracts/server.crt 20 | - node_modules/knex/lib/bin/cli.js migrate:latest 21 | - docker info 22 | override: 23 | - SSL_KEY=/tmp/contracts/server.key SSL_CERT=/tmp/contracts/server.crt CONTRACTS_STORAGE=/tmp/contracts/ npm test 24 | - npm install -g codius 25 | - docker build -t codius/codius-host:$CIRCLE_SHA1 . 26 | - docker images 27 | - docker run -v /tmp/contracts:/contracts -e SSL_KEY=/contracts/server.key -e SSL_CERT=/contracts/server.crt -d -p 8080:8080 codius/codius-host:$CIRCLE_SHA1; sleep 5 28 | - ./test-contract-upload.sh 29 | post: 30 | - istanbul-coveralls 31 | deployment: 32 | docker: 33 | branch: master 34 | commands: 35 | - docker login -e $DOCKER_EMAIL -u $DOCKER_USERNAME -p $DOCKER_PASSWORD 36 | - docker tag codius/codius-host:$CIRCLE_SHA1 codius/codius-host:latest 37 | - docker push codius/codius-host:$CIRCLE_SHA1 38 | - docker push codius/codius-host:latest 39 | - ./deploy-to-testnet.sh 40 | -------------------------------------------------------------------------------- /migrations/20140802170601_add_tokens.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | 'use strict'; 21 | 22 | exports.up = function(knex, Promise) { 23 | return knex.schema.createTable('tokens', function (t) { 24 | t.increments().primary(); 25 | t.string('token', 20).unique(); 26 | t.integer('contract_id').unsigned().index().references('id').inTable('contracts'); 27 | t.integer('balance_id').unsigned().index().references('id').inTable('balances'); 28 | t.integer('parent_id').unsigned().index().references('id').inTable('tokens'); 29 | }); 30 | }; 31 | 32 | exports.down = function(knex, Promise) { 33 | return knex.schema.dropTable('tokens'); 34 | }; 35 | -------------------------------------------------------------------------------- /test/http/host_meta.js: -------------------------------------------------------------------------------- 1 | var nconf = require('../../lib/config'); 2 | var Promise = require('bluebird').Promise; 3 | var codius = require('../../') 4 | var assert = require('assert') 5 | var supertest = require('supertest') 6 | var server = require('../../lib/application')(codius) 7 | var http = supertest(server) 8 | var fs = require('fs') 9 | var path = require('path') 10 | var genkey = require('../genkey'); 11 | 12 | describe('Host Meta and Root Endpoints', function() { 13 | var version, publicKey 14 | 15 | before(function() { 16 | version = JSON.parse(fs.readFileSync(path.join(__dirname+'/../../package.json')).toString()).version; 17 | }); 18 | 19 | beforeEach(function() { 20 | return genkey().then(function(v) { 21 | publicKey = v.certContents; 22 | nconf.set('ssl:cert', v.certPath); 23 | nconf.set('ssl:key', v.keyPath); 24 | }); 25 | }) 26 | 27 | it('should redirect the root to host meta', function(done) { 28 | 29 | http.get('/').end(function(error, response) { 30 | assert.strictEqual(response.statusCode, 301) 31 | assert.strictEqual(response.headers.location, '/.well-known/host-meta.json') 32 | done() 33 | }) 34 | }) 35 | 36 | it('should get the host meta', function(done) { 37 | http.get('/.well-known/host-meta.json').end(function(error, response) { 38 | assert.strictEqual(response.statusCode, 200) 39 | assert.strictEqual(response.body.properties.documentation, 'https://codius.org/docs/using-codius/getting-started') 40 | assert.strictEqual(response.body.properties.version, version) 41 | assert.strictEqual(response.body.properties.public_key, publicKey) 42 | done() 43 | }) 44 | }) 45 | }) 46 | 47 | -------------------------------------------------------------------------------- /test/models/debit.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var Credit = require(path.join(__dirname, '/../../models/credit')).model; 3 | var Debit = require(path.join(__dirname, '/../../models/debit')).model; 4 | var Token = require(path.join(__dirname, '/../../models/token')).model; 5 | var assert = require('assert'); 6 | 7 | describe('Debit Model', function() { 8 | var token; 9 | 10 | before(function(done) { 11 | new Token().save().then(function(_token) { 12 | token = _token; 13 | done(); 14 | }); 15 | }); 16 | 17 | after(function(done) { 18 | token.destroy().then(function() { 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should subtract from a balance', function(done) { 24 | 25 | token.getBalance().then(function(balance) { 26 | 27 | balance.credit(5).then(function() { 28 | Debit.debitBalance(balance, 1).then(function(debit) { 29 | assert.strictEqual(debit.get('amount'), 1); 30 | balance.refresh().then(function(balance) { 31 | assert.strictEqual(balance.get('balance'), 4); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | }); 37 | }); 38 | 39 | it('should reject with a negative amount', function(done) { 40 | 41 | new Debit({ amount: -5 }).save().error(function(error) { 42 | assert(error); 43 | assert.strictEqual(error.message, 'amount must be greater than zero'); 44 | done(); 45 | }); 46 | }); 47 | 48 | it('should reject with an empty amount', function(done) { 49 | 50 | new Debit().save().error(function(error) { 51 | assert(error); 52 | assert.strictEqual(error.message, 'amount must be greater than zero'); 53 | done(); 54 | }); 55 | }); 56 | }); 57 | 58 | -------------------------------------------------------------------------------- /models/ledger.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var bookshelf = require('../lib/db').bookshelf; 21 | var Address = require(__dirname+'/address') 22 | 23 | var Ledger = bookshelf.Model.extend({ 24 | tableName: 'ledgers', 25 | 26 | registerAddress: function(token, address) { 27 | return new Address.model({ 28 | token_id: token.get('id'), 29 | ledger_id: this.get('id'), 30 | address: address 31 | }) 32 | .save() 33 | }, 34 | 35 | addresses: function() { 36 | return this.hasMany(Address.model) 37 | } 38 | }); 39 | 40 | Ledger.findOrCreate = function(options) { 41 | var ledger = new Ledger(options) 42 | 43 | return ledger.fetch().then(function(ledger) { 44 | if (ledger) { return ledger } 45 | return new Ledger(options).save() 46 | }) 47 | } 48 | 49 | exports.model = Ledger; 50 | 51 | -------------------------------------------------------------------------------- /lib/token.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var random = require('random-js')(); 21 | 22 | var TOKEN_REGEX = exports.TOKEN_REGEX = /^[a-z0-9]{6}-[a-z0-9]{6}-[a-z0-9]{6}$/i; 23 | 24 | var TOKEN_SEGMENT_MIN = exports.TOKEN_SEGMENT_MIN = parseInt('100000', 36); 25 | var TOKEN_SEGMENT_MAX = exports.TOKEN_SEGMENT_MAX = parseInt('zzzzzz', 36); 26 | 27 | /** 28 | * Generate a segment for a Codius token. 29 | * 30 | * It's a random base36 string of length 6. 31 | */ 32 | var generateSegment = exports.generateSegment = function () { 33 | return random.integer(TOKEN_SEGMENT_MIN, TOKEN_SEGMENT_MAX).toString(36); 34 | }; 35 | 36 | /** 37 | * Generate a random Codius token. 38 | * 39 | * A Codius token is a token of the form [a-z0-9]{6}-[a-z0-9]{6}-[a-z0-9]{6}. 40 | */ 41 | exports.generateToken = function () { 42 | return generateSegment() + '-' + generateSegment() + '-' + generateSegment(); 43 | }; 44 | -------------------------------------------------------------------------------- /test/http/billing_service.js: -------------------------------------------------------------------------------- 1 | var uuid = require('uuid') 2 | var Promise = require('bluebird') 3 | var codius = require(__dirname+'/../../') 4 | var assert = require('assert') 5 | var expect = require('chai').expect; 6 | var supertest = require('supertest-as-promised') 7 | var Server = require(__dirname+'/../../billing-service/server'); 8 | 9 | describe('Billing Service HTTP Interface', function() { 10 | var token, server, http; 11 | 12 | before(function() { 13 | return new codius.Token({ token: uuid.v4() }).save() 14 | .then(function(model) { 15 | token = model 16 | }) 17 | }) 18 | 19 | beforeEach(function() { 20 | server = Server(codius); 21 | http = supertest(server); 22 | }); 23 | 24 | it('should credit a token balance', function() { 25 | var url = '/contracts/'+token.get('token')+'/credits' 26 | return http 27 | .post(url) 28 | .send({ amount: 100 }) 29 | .expect(200) 30 | .then(function(response) { 31 | return expect(getBalance(token)).to.eventually.equal(100); 32 | }); 33 | }) 34 | 35 | it('should debit a token balance', function() { 36 | return http 37 | .post('/contracts/'+token.get('token')+'/debits') 38 | .send({ amount: 50 }) 39 | .expect(200) 40 | .then(function(response) { 41 | return expect(getBalance(token)).to.eventually.equal(50); 42 | }); 43 | }) 44 | 45 | it('should overdraft with an error', function() { 46 | return http 47 | .post('/contracts/'+token.get('token')+'/debits') 48 | .send({ amount: 75 }) 49 | .expect(500) 50 | .then(function(response) { 51 | assert.strictEqual(response.body.error, 'overdraft') 52 | return expect(getBalance(token)).to.eventually.equal(0); 53 | }); 54 | }) 55 | 56 | function getBalance(token) { 57 | return http.get('/contracts/'+token.get('token')) 58 | .then(function(response) { 59 | return response.body.balance 60 | }) 61 | } 62 | }) 63 | -------------------------------------------------------------------------------- /lib/engine.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var config = require('./config'); 21 | var log = require('./log'); 22 | var contractStorage = require('./contract_storage'); 23 | 24 | var codiusEngine = require('codius-engine'); 25 | var EngineConfig = codiusEngine.Config; 26 | var Compiler = codiusEngine.Compiler; 27 | var FileManager = codiusEngine.FileManager; 28 | var FileHash = codiusEngine.FileHash; 29 | var Engine = codiusEngine.Engine; 30 | 31 | // Prepare engine configuration 32 | var engineConfig = new EngineConfig(config.get('engine')); 33 | engineConfig.logger = log.winston; 34 | 35 | 36 | // Boot Codius engine 37 | var compiler = new Compiler(engineConfig); 38 | var fileManager = new FileManager(engineConfig); 39 | var engine = new Engine(engineConfig); 40 | engine.setContractStorage(contractStorage); 41 | 42 | exports.engineConfig = engineConfig; 43 | exports.compiler = compiler; 44 | exports.fileManager = fileManager; 45 | exports.engine = engine; 46 | -------------------------------------------------------------------------------- /processes/billing_bitcoind.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | module.exports = function(codius) { 21 | 22 | if (codius.features.isEnabled('BITCOIN_BILLING')) { 23 | 24 | // during development of this feature use a path to the module instead of npm 25 | var library = process.env['CODIUS_BILLING_BITCOIND_PATH'] || 'codius-billing-bitcoind' 26 | 27 | var CodiusBillingBitcoind = require(library); 28 | 29 | var bitcoind = { 30 | host: process.env['BITCOIND_HOST'], 31 | port: process.env['BITCOIND_PORT'], 32 | user: process.env['BITCOIND_USER'], 33 | pass: process.env['BITCOIND_PASS'], 34 | confirmations: process.env['BITCOIND_CONFIRMATIONS'] || 1 35 | } 36 | 37 | var billing = new CodiusBillingBitcoind(codius, bitcoind) 38 | 39 | codius.events.on('contract:created', function(token) { 40 | billing.registerContract(token) 41 | }) 42 | 43 | billing.processPayments() 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /routes/get_token_metadata.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var Token = require(path.join(__dirname, '/../models/token')).model; 3 | var amortizer = require(path.join(__dirname, '/../lib/amortizer')); 4 | var features = require(path.join(__dirname, '/../lib/features')); 5 | var request = require('request') 6 | var _ = require('lodash') 7 | 8 | module.exports = function(req, res, next) { 9 | 10 | var token = req.params.token; 11 | if (!token) { 12 | res.status(400).json({ 13 | message: "Must supply contract token to retrieve metadata" 14 | }); 15 | return; 16 | } 17 | 18 | new Token({token: token}).fetch({ 19 | withRelated: ['balance', 'contract'] 20 | }).then(function (model) { 21 | if (!model) { 22 | res.status(404).json({ 23 | message: "Token not found" 24 | }); 25 | return; 26 | } else { 27 | var metadata = { 28 | token: token, 29 | hash: model.related('contract').get('hash') 30 | } 31 | amortizer.checkTokenBalance(model).then(function(balance){ 32 | metadata.compute_units = balance; 33 | model.related('addresses').fetch({ withRelated: ['ledger'] }).then(function(addresses) { 34 | if (addresses.models.length > 0) { 35 | metadata.payment_addresses = _.map(addresses.models, function(address) { 36 | return address.related('ledger').get('name')+':'+address.get('address') 37 | }) 38 | } 39 | var getInstanceUrl = 'http://127.0.0.1:' + ((parseInt(process.env.PORT) || 5000) + 30) + '/instances/' + token; 40 | request.get(getInstanceUrl, function(err, result, body) { 41 | if (!err && result.statusCode == 200) { 42 | var instance = JSON.parse(body).instance; 43 | if (instance && instance.state) { 44 | metadata.state = instance.state; 45 | } 46 | } 47 | res.status(200).json(metadata); 48 | }) 49 | }); 50 | }) 51 | } 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /compute-service/controllers/instances.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird') 2 | 3 | module.exports = function(codius) { 4 | 5 | function getToken(token) { 6 | return new codius.Token({ token: token }).fetch() 7 | .then(function(token) { 8 | if (token) { 9 | return Promise.resolve(token) 10 | } else { 11 | return Promise.reject(new Error('token not found')) 12 | } 13 | }) 14 | } 15 | 16 | return { 17 | create: function(req, res, next) { 18 | getToken(req.body.token).then(function(token) { 19 | return codius.compute.startInstance(token, 20 | req.body.container_uri, 21 | req.body.type, 22 | req.body.vars, 23 | req.body.port) 24 | .then(function(instance) { 25 | res.send({ 26 | success: true, 27 | instance: instance 28 | }) 29 | }) 30 | }) 31 | .error(next) 32 | }, 33 | 34 | stop: function(req, res, next) { 35 | getToken(req.params.token).then(function(token) { 36 | return codius.compute.stopInstance(token) 37 | .then(function(state) { 38 | res.send({ 39 | success: true, 40 | instance: { 41 | state: state 42 | } 43 | }) 44 | }) 45 | }) 46 | .error(next) 47 | }, 48 | 49 | index: function(req, res, next) { 50 | return codius.compute.getInstances().then(function(instances) { 51 | res.status(200).send({ 52 | success: true, 53 | instances: instances 54 | }) 55 | }) 56 | .error(next) 57 | }, 58 | 59 | show: function(req, res, next) { 60 | getToken(req.params.token).then(function(token) { 61 | return codius.compute.getInstance(token) 62 | .then(function(instance) { 63 | res.status(200).send({ 64 | success: true, 65 | instance: instance 66 | }) 67 | }) 68 | }) 69 | .error(next) 70 | } 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var path = require('path'); 21 | 22 | module.exports.Server = require(path.join(__dirname, 'server')); 23 | module.exports.Application = require(path.join(__dirname, 'application')); 24 | module.exports.amortizer = require(path.join(__dirname, 'amortizer')); 25 | module.exports.BillingService = require(path.join(__dirname, 'billing_service')); 26 | module.exports.compute = require(path.join(__dirname, 'compute_service')); 27 | module.exports.Contract = require(path.join(__dirname, '/../models/contract')).model; 28 | module.exports.Token = require(path.join(__dirname, '/../models/token')).model; 29 | module.exports.Ledger = require(path.join(__dirname, '/../models/ledger')).model; 30 | module.exports.Address = require(path.join(__dirname, '/../models/address')).model; 31 | module.exports.features = require(path.join(__dirname, 'features')); 32 | module.exports.config = require(path.join(__dirname, 'config')); 33 | module.exports.logger = require(path.join(__dirname, 'log')).winston; 34 | module.exports.events = require(path.join(__dirname, 'events')); 35 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codius-host", 3 | "version": "1.2.0", 4 | "description": "Codius host for Node.js", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "bin/codius-host start", 8 | "keygen": "openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt", 9 | "test": "istanbul cover _mocha -- -R spec test/ test/models/*.js test/http/*.js" 10 | }, 11 | "bin": { 12 | "codius-host": "bin/codius-host" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/codius/host.git" 17 | }, 18 | "keywords": [ 19 | "codius", 20 | "javascript", 21 | "smart", 22 | "oracle", 23 | "contracts", 24 | "distributed", 25 | "autonomous", 26 | "trustless", 27 | "cloud", 28 | "virtual" 29 | ], 30 | "author": "Stefan Thomas ", 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/codius/host/issues" 34 | }, 35 | "homepage": "https://github.com/codius/host", 36 | "devDependencies": { 37 | "chai": "~1.9.1", 38 | "chai-as-promised": "^4.2.0", 39 | "istanbul": "~0.2.16", 40 | "mocha": "~1.20.1", 41 | "sinon": "~1.10.2", 42 | "sinon-chai": "~2.5.0", 43 | "sqlite3": "^2.2.4", 44 | "supertest-as-promised": "^1.0.0", 45 | "temp": "^0.8.1" 46 | }, 47 | "dependencies": { 48 | "base64url": "0.0.6", 49 | "bitcoinjs-lib": "^1.4.3", 50 | "bluebird": "^2.9.12", 51 | "bookshelf": "^0.7.7", 52 | "bridges-application": "^0.2.1", 53 | "bridges-express": "^0.3.1", 54 | "chalk": "^0.5.1", 55 | "codius-billing-bitcoind": "^0.3.1", 56 | "codius-engine": "^1.2.1", 57 | "commander": "^2.6.0", 58 | "concat-stream": "^1.4.6", 59 | "express": "^4.5.1", 60 | "flipper": "0.0.3", 61 | "knex": "^0.7.4", 62 | "lodash": "^3.2.0", 63 | "morgan": "^1.2.2", 64 | "nconf": "^0.6.9", 65 | "nodemon": "^1.2.1", 66 | "pg.js": "^3.4.1", 67 | "random-js": "^1.0.4", 68 | "request": "^2.39.0", 69 | "ripple-account-monitor": "^2.2.0", 70 | "sinon": "^1.12.2", 71 | "superagent": "^0.21.0", 72 | "supertest": "^0.15.0", 73 | "tar-stream": "^0.4.4", 74 | "underscore": "^1.7.0", 75 | "uuid": "^2.0.1", 76 | "winston": "^0.7.3" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/billing_service.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var Promise = require('bluebird'); 3 | var Overdraft = require(path.join(__dirname, 'overdraft')); 4 | var Debit = require(path.join(__dirname, '/../models/debit')).model; 5 | var Credit = require(path.join(__dirname, '/../models/credit')).model; 6 | var Balance = require(path.join(__dirname, '/../models/balance')).model; 7 | 8 | function BillingService() {} 9 | 10 | BillingService.Overdraft = Overdraft; 11 | 12 | BillingService.prototype.credit = function(token, amount) { 13 | return new Promise(function(resolve, reject) { 14 | ensureToken(token); 15 | validateAmount(amount); 16 | 17 | token.getBalance().then(function(balance) { 18 | return balance.credit(amount) 19 | }) 20 | .then(resolve) 21 | .error(reject); 22 | }); 23 | } 24 | 25 | BillingService.prototype.debit = function(token, amount) { 26 | var this_ = this; 27 | return new Promise(function(resolve, reject) { 28 | ensureToken(token); 29 | validateAmount(amount); 30 | 31 | token.getBalance().then(function(balance) { 32 | if (balance.get('balance') >= amount) { 33 | return balance.debit(amount); 34 | } else { 35 | return balance.debit(balance.get('balance')).then(function(debit) { 36 | reject(new Overdraft(amount - balance.get('balance'), debit)); 37 | }); 38 | } 39 | }) 40 | .then(resolve) 41 | .error(reject) 42 | }); 43 | } 44 | 45 | BillingService.prototype.getBalance = function(token) { 46 | return new Promise(function(resolve, reject) { 47 | ensureToken(token); 48 | token.getBalance().then(resolve).error(reject); 49 | }); 50 | } 51 | 52 | BillingService.prototype.getCredits = function(token) { 53 | return new Promise(function(resolve, reject) { 54 | ensureToken(token); 55 | token.getBalance().then(function(balance) { 56 | return balance.related('credits').fetch(); 57 | }) 58 | .then(resolve).error(reject); 59 | }); 60 | } 61 | 62 | BillingService.prototype.getDebits = function(token) { 63 | return new Promise(function(resolve, reject) { 64 | ensureToken(token); 65 | token.getBalance().then(function(balance) { 66 | return balance.related('debits').fetch(); 67 | }) 68 | .then(resolve).error(reject); 69 | }); 70 | } 71 | 72 | function ensureToken(token) { 73 | if (!token) { throw new Error('token must be provided') } 74 | } 75 | 76 | function validateAmount(amount) { 77 | if (!(Number(amount) > 0)) { throw new Error('amount must be greater than 0') } 78 | } 79 | 80 | module.exports = BillingService; 81 | -------------------------------------------------------------------------------- /models/token.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var bookshelf = require('../lib/db').bookshelf; 21 | var Balance = require(__dirname+'/balance'); 22 | var Contract = require(__dirname+'/contract'); 23 | var Address = require(__dirname+'/address'); 24 | var events = require(__dirname+'/../lib/events'); 25 | var Promise = require('bluebird'); 26 | 27 | var Token = bookshelf.Model.extend({ 28 | initialize: function() { 29 | this.on('created', this.attachBalance); 30 | this.on('created', this.emitCreated); 31 | }, 32 | tableName: 'tokens', 33 | balance: function () { 34 | return this.hasOne(Balance.model); 35 | }, 36 | addresses: function() { 37 | return this.hasMany(Address.model); 38 | }, 39 | contract: function () { 40 | return this.belongsTo(Contract.model); 41 | }, 42 | emitCreated: function(token) { 43 | events.emit('contract:created', token); 44 | }, 45 | attachBalance: function() { 46 | var this_ = this; 47 | return new Balance.model({ token_id: this.get('id') }) 48 | .save() 49 | .then(function(balance) { 50 | this_.set('balance_id', balance.get('id')); 51 | return this_.save(); 52 | }); 53 | }, 54 | getBalance: function() { 55 | return new Balance.model({ token_id: this.get('id') }).fetch() 56 | .then(function(balance) { 57 | if (!balance) { 58 | return Promise.reject(new Error('balance not found')); 59 | } 60 | return Promise.resolve(balance); 61 | }) 62 | } 63 | }); 64 | 65 | exports.model = Token; 66 | -------------------------------------------------------------------------------- /models/balance.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var path = require('path'); 21 | var bookshelf = require(path.join(__dirname, '/../lib/db')).bookshelf; 22 | var Credit = require(path.join(__dirname, 'credit')); 23 | var Debit = require(path.join(__dirname, 'debit')); 24 | var events = require(path.join(__dirname+'/../lib/events')); 25 | var Token = require(path.join(__dirname, 'token')); 26 | var Promise = require('bluebird'); 27 | 28 | var Balance = bookshelf.Model.extend({ 29 | tableName: 'balances', 30 | defaults: { 31 | balance: 0 32 | }, 33 | token: function () { 34 | return this.belongsTo(Token.model); 35 | }, 36 | debits: function() { 37 | return this.hasMany(Debit.model); 38 | }, 39 | credits: function() { 40 | return this.hasMany(Credit.model); 41 | }, 42 | credit: function(amount) { 43 | var self = this; 44 | return Credit.model.creditBalance(self, amount).then(function(credit) { 45 | return self.refresh().then(function(balance) { 46 | events.emit('balance:credited', balance); 47 | return credit; 48 | }) 49 | }); 50 | }, 51 | debit: function(amount) { 52 | var self = this; 53 | return Debit.model.debitBalance(self, amount).then(function(debit) { 54 | return self.refresh().then(function(balance) { 55 | events.emit('balance:debited', balance); 56 | return debit; 57 | }) 58 | }); 59 | }, 60 | refresh: function() { 61 | return new Balance({ id: this.get('id') }).fetch() 62 | } 63 | }); 64 | 65 | exports.model = Balance; 66 | -------------------------------------------------------------------------------- /test/application.js: -------------------------------------------------------------------------------- 1 | var nconf = require('../lib/config'); 2 | nconf.set('db:connection:filename', ':memory:'); 3 | 4 | var db = require('../lib/db'); 5 | 6 | var CodiusHost = require(__dirname+'/../'); 7 | var express = require('express'); 8 | var sinon = require('sinon'); 9 | var assert = require('assert'); 10 | var supertest = require('supertest-as-promised'); 11 | var Contract = require(__dirname+'/../models/contract').model; 12 | 13 | describe('Codius Host Express Application', function() { 14 | var application, http, token; 15 | 16 | before(function() { 17 | return db.knex.migrate.rollback(db.conf); 18 | }) 19 | 20 | beforeEach(function() { 21 | return db.knex.migrate.latest(db.conf).then(function() { 22 | application = new CodiusHost.Application(); 23 | http = supertest(application); 24 | }); 25 | }); 26 | 27 | afterEach(function() { 28 | return db.knex.migrate.rollback(db.conf); 29 | }); 30 | 31 | it('should initialize an express application', function() { 32 | assert.strictEqual(typeof application.listen, 'function'); 33 | }); 34 | 35 | it('should expose a health check route', function() { 36 | return http 37 | .get('/health') 38 | .expect(200); 39 | }); 40 | 41 | it('should expose a token generation route', function() { 42 | var contractHash = '427c7a0bfa92621f93fac7ed35e42a6d4fc4fef522b89ade12776367399014ef'; 43 | return new Contract({hash: contractHash}).save().then(function (contract) { 44 | return http 45 | .post('/token?contract='+contractHash) 46 | .expect(200) 47 | .then(function(response) { 48 | token = response.body.token; 49 | return contract.destroy(); 50 | }); 51 | }) 52 | }); 53 | 54 | it('should expose a contract metadata route', function() { 55 | var contractHash = '427c7a0bfa92621f93fac7ed35e42a6d4fc4fef522b89ade12776367399014ef'; 56 | return new Contract({hash: contractHash}).save().then(function (contract) { 57 | return http 58 | .post('/token?contract='+contractHash) 59 | .expect(200) 60 | .then(function(response) { 61 | token = response.body.token; 62 | return http 63 | .get('/token/'+token) 64 | .expect(200).then(function() { 65 | return contract.destroy(); 66 | }); 67 | }); 68 | }) 69 | }); 70 | 71 | it.skip('should not upload an empty contract', function() { 72 | return http 73 | .post('/contract') 74 | .expect(500) 75 | .then(function(response) { 76 | assert.strictEqual(response.body.success, false); 77 | assert.strictEqual(response.body.error, 'no contract provided'); 78 | }); 79 | }); 80 | }); 81 | 82 | -------------------------------------------------------------------------------- /test/amortizer.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird').Promise; 2 | 3 | Promise.longStackTraces(); 4 | var nconf = require('../lib/config'); 5 | nconf.set('db:connection:filename', ':memory:'); 6 | 7 | var db = require('../lib/db'); 8 | 9 | var uuid = require('uuid'); 10 | var path = require('path'); 11 | var Token = require(path.join(__dirname, '/../models/token')).model; 12 | var amortizer = require(path.join(__dirname, '/../lib/amortizer')); 13 | var assert = require('assert'); 14 | 15 | describe('Amortizer', function() { 16 | var token, startTime, startBalance; 17 | 18 | before(function() { 19 | return db.knex.migrate.rollback(db.conf); 20 | }); 21 | 22 | beforeEach(function() { 23 | return db.knex.migrate.latest(db.conf).then(function() { 24 | startTime = Date.now(); 25 | startBalance = 10; 26 | amortizer._instances[token] = { 27 | lastCheckedBalance: startBalance, 28 | lastChargedTime: startTime 29 | } 30 | return new Token({ token: uuid.v4() }).save().then(function(token_) { 31 | token = token_; 32 | startTime = Date.now(); 33 | amortizer._instances[token.get('token')] = { 34 | lastCheckedBalance: 10, 35 | lastChargedTime: startTime 36 | } 37 | return token.getBalance().then(function(balance) { 38 | balance.credit(10); 39 | }); 40 | }); 41 | }); 42 | }); 43 | 44 | afterEach(function() { 45 | return token.destroy().then(function() { 46 | return db.knex.migrate.rollback(db.conf); 47 | }); 48 | }); 49 | 50 | it('should check a token\'s balance from the database', function() { 51 | return new Token({ token: uuid.v4() }).save().then(function(token_) { 52 | return amortizer.checkTokenBalance(token_).then(function(balance) { 53 | assert.strictEqual(balance, 0); 54 | }) 55 | .then(function() { 56 | return token_.destroy(); 57 | }); 58 | }); 59 | }); 60 | 61 | it('should calculate the amount to charge a running instance', function() { 62 | var charge = amortizer.calculateCharge(token); 63 | assert(charge <= Math.ceil((Date.now() - startTime) / 100)); 64 | }); 65 | 66 | it('should charge a running instance\'s balance', function() { 67 | return Promise.delay(10).then(function() { 68 | return amortizer.chargeToken(token).then(function(balance) { 69 | assert(startBalance - Math.ceil((Date.now() - startTime) / 100) <= balance); 70 | }); 71 | }); 72 | }); 73 | 74 | it('should check a running instance\'s current balance', function() { 75 | return Promise.delay(10).then(function() { 76 | return amortizer.checkTokenBalance(token).then(function(balance) { 77 | assert(startBalance - Math.ceil((Date.now() - startTime) / 100) <= balance); 78 | }); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /lib/contract_storage.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var Contract = require('../models/contract').model; 21 | var ContractStorageEntry = require('../models/contract_storage_entry').model; 22 | 23 | exports.getItem = getItem; 24 | exports.setItem = setItem; 25 | exports.removeItem = removeItem; 26 | exports.clear = clear; 27 | exports.key = key; 28 | 29 | function getItem(contractHash, key, callback) { 30 | new Contract({ hash: contractHash }).fetch().then(function (contract) { 31 | if (!contract) { 32 | throw new Error('Unknown contract hash'); 33 | } 34 | 35 | return new ContractStorageEntry({key: key, contract_id: contract.id}).fetch(); 36 | }).then(function (entry) { 37 | if (!entry) { 38 | callback(null, void(0)); 39 | return; 40 | } 41 | 42 | callback(null, entry.get('value')); 43 | }).catch(callback); 44 | } 45 | 46 | function setItem(contractHash, key, value, callback) { 47 | var contract; 48 | new Contract({ hash: contractHash }).fetch().then(function (contract_) { 49 | contract = contract_; 50 | 51 | if (!contract) { 52 | throw new Error('Unknown contract hash'); 53 | } 54 | 55 | return new ContractStorageEntry({key: key, contract_id: contract.id}).fetch(); 56 | }).then(function (entry) { 57 | if (!entry) { 58 | return new ContractStorageEntry({key: key, value: value, contract_id: contract.id}).save(); 59 | } else { 60 | return entry.save({value: value}); 61 | } 62 | }).then(function () { 63 | callback(); 64 | }).catch(callback); 65 | } 66 | 67 | function removeItem(contractHash, key, callback) { 68 | callback(new Error('contract_storage: removeItem is not implemented!')); 69 | } 70 | 71 | function clear(contractHash, callback) { 72 | callback(new Error('contract_storage: clear is not implemented!')); 73 | } 74 | 75 | function key(contractHash, index, callback) { 76 | callback(new Error('contract_storage: key is not implemented!')); 77 | } 78 | -------------------------------------------------------------------------------- /routes/post_token.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | var path = require('path'); 20 | var tokenLib = require(path.join(__dirname, '/../lib/token')); 21 | 22 | var Token = require(path.join(__dirname, '/../models/token')).model; 23 | var Contract = require(path.join(__dirname, '/../models/contract')).model; 24 | var Balance = require(path.join(__dirname, '/../models/balance')).model; 25 | var BillingService = require(path.join(__dirname, '/../lib/billing_service')); 26 | var engine = require(path.join(__dirname, '/../lib/engine')); 27 | 28 | /** 29 | * Request a token. 30 | * 31 | * A token is a multi-use endpoint for interacting with a contract via HTTP. 32 | */ 33 | module.exports = function (req, res) { 34 | var config = req.app.get('config'); 35 | 36 | function getUniqueToken() { 37 | var token = tokenLib.generateToken(); 38 | 39 | return new Token({token: token}).fetch().then(function (model) { 40 | if (model !== null) { 41 | return getUniqueToken(); 42 | } else return token; 43 | }); 44 | } 45 | 46 | // First we check if the contract actually exists 47 | new Contract({hash: req.query.contract}).fetch().then(function (contract) { 48 | if (!contract) { 49 | // Contract doesn't exist 50 | res.status(400).json({ 51 | message: "Unknown contract hash" 52 | }); 53 | } else { 54 | // TODO: clean up this mess of returns 55 | return getUniqueToken().then(function (token) { 56 | return Token.forge({token: token, contract_id: contract.get('id')}).save().then(function(token){ 57 | if (config.get('starting_cpu_balance') > 0) { 58 | return new BillingService().credit(token, config.get('starting_cpu_balance')).then(function() { 59 | return token; 60 | }); 61 | } else return token; 62 | }); 63 | }).then(function (token) { 64 | // All done! 65 | res.status(200).json({ 66 | token: token.get('token') 67 | }); 68 | }); 69 | } 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | var Promise = require('bluebird'); 3 | var http = require('http'); 4 | var tls = require('tls'); 5 | var net = require('net'); 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | var Application = require(path.join(__dirname, 'application')); 9 | 10 | // Load application components - order matters 11 | // Log should be loaded before any components that might log during startup 12 | 13 | var config = require(path.join(__dirname, 'config')); 14 | var log = require(path.join(__dirname, 'log')); 15 | var db = require(path.join(__dirname, 'db')); 16 | var tokenLib = require(path.join(__dirname, 'token')); 17 | 18 | function CodiusHost() {}; 19 | 20 | CodiusHost.prototype.start = function(compute) { 21 | 22 | return new Promise(function(resolve, reject) { 23 | 24 | var app = new Application(); 25 | 26 | var unique = 0, internalServer; 27 | // Run migrations 28 | db.knex.migrate.latest().then(function () { 29 | // This is the internal HTTP server. External people will not connect to 30 | // this directly. Instead, they will connect to our TLS port and if they 31 | // weren't specifying a token, we'll assume they want to talk to the host 32 | // and route the request to this HTTP server. 33 | 34 | // A port value of zero means a randomly assigned port 35 | internalServer = http.createServer(app); 36 | return Promise.promisifyAll(internalServer).listenAsync(0, '127.0.0.1'); 37 | }).then(function () { 38 | // Create public-facing (TLS) server 39 | var tlsServer = tls.createServer({ 40 | ca: config.get('ssl').ca && fs.readFileSync(config.get('ssl').ca), 41 | key: fs.readFileSync(config.get('ssl').key), 42 | cert: fs.readFileSync(config.get('ssl').cert) 43 | }); 44 | tlsServer.listen(config.get('port'), function () { 45 | winston.info('Codius host running on port '+config.get('port')); 46 | resolve(); 47 | }); 48 | var internalServerAddress = internalServer.address(); 49 | 50 | tlsServer.on('secureConnection', function (cleartextStream) { 51 | // Is this connection meant for a contract? 52 | // 53 | // We determine the contract being addressed using the Server Name 54 | // Indication (SNI) 55 | if (cleartextStream.servername && tokenLib.TOKEN_REGEX.exec(cleartextStream.servername.split('.')[0])) { 56 | var token = cleartextStream.servername.split('.')[0] 57 | compute.handleConnection(token, cleartextStream); 58 | 59 | // Otherwise it must be meant for the host 60 | } else { 61 | // Create a connection to the internal HTTP server 62 | var client = net.connect(internalServerAddress.port, 63 | internalServerAddress.address); 64 | 65 | // And just bidirectionally associate it with the incoming cleartext connection. 66 | cleartextStream.pipe(client); 67 | client.pipe(cleartextStream); 68 | } 69 | }); 70 | }).done(); 71 | }); 72 | }; 73 | 74 | module.exports = CodiusHost; 75 | 76 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var path = require('path'); 21 | var nconf = require('nconf'); 22 | 23 | // First consider commandline arguments and environment variables, respectively. 24 | nconf.argv().env(); 25 | 26 | // Then load configuration from a designated file. 27 | nconf.file({ file: 'config.json' }); 28 | 29 | nconf.defaults({ 30 | "log_format": "dev", 31 | "engine": { 32 | "contractsFilesystemPath": nconf.get('CONTRACTS_STORAGE'), 33 | "disableNacl": true 34 | }, 35 | "compute_units_per_bitcoin": 50000000, 36 | "compute_units_per_xrp": 2000, 37 | "milliseconds_per_compute_unit": 1000, 38 | "starting_cpu_balance": 0, 39 | 'SSL_CERT': path.join(__dirname, '/../server.crt'), 40 | 'SSL_KEY': path.join(__dirname, '/../server.key'), 41 | 'CONTRACTS_STORAGE': 'contract_filesystem/' 42 | }); 43 | 44 | nconf.set("ssl", { 45 | "cert": nconf.get('SSL_CERT'), 46 | "key": nconf.get('SSL_KEY') 47 | }) 48 | 49 | if (nconf.get('NODE_ENV') === 'fig') { 50 | // nconf doesn't support multiple layers of defaults 51 | // https://github.com/flatiron/nconf/issues/81 52 | nconf.add('db_defaults', {'type': 'literal', 53 | // Port for incoming TLS (e.g. HTTPS) connections 54 | "port": process.env.PORT || 2633, 55 | "db": { 56 | "client": 'pg', 57 | "connection": { 58 | "host": process.env.CODIUSHOST_DB_1_PORT_5432_TCP_ADDR, 59 | "port": process.env.CODIUSHOST_DB_1_PORT_5432_TCP_PORT, 60 | "database": 'docker', 61 | "user": 'docker', 62 | "password": 'docker' 63 | }, 64 | "pool": { 65 | "min": 2, 66 | "max": 10 67 | } 68 | } 69 | }); 70 | } else if (nconf.get('NODE_ENV') === 'beanstalk') { 71 | // nconf doesn't support multiple layers of defaults 72 | // https://github.com/flatiron/nconf/issues/81 73 | nconf.add('db_defaults', {'type': 'literal', 74 | "port": process.env.CODIUS_PORT || process.env.PORT || 443, 75 | "db": { 76 | "client": "pg", 77 | "connection": { 78 | "host": process.env.RDS_HOSTNAME, 79 | "port": process.env.RDS_PORT, 80 | "database": process.env.RDS_DB_NAME, 81 | "user": process.env.RDS_USERNAME, 82 | "password": process.env.RDS_PASSWORD 83 | }, 84 | "pool": { 85 | "min": 2, 86 | "max": 10 87 | } 88 | }, 89 | "ssl": { 90 | "ca": path.resolve(__dirname, '../ca.crt'), 91 | "cert": path.resolve(__dirname, '../server.crt'), 92 | "key": path.resolve(__dirname, '../server.key') 93 | }, 94 | "bitcoin_bip32_extended_public_key": process.env.BITCOIN_EXTENDED_PUBLIC_KEY 95 | }); 96 | } else { 97 | nconf.add('db_defaults', {'type': 'literal', 98 | // Port for incoming TLS (e.g. HTTPS) connections 99 | 'port': process.env.PORT || 2633, 100 | 'db': { 101 | client: 'sqlite3', 102 | connection: { 103 | filename: path.join(__dirname, '../dev.sqlite3') 104 | } 105 | } 106 | }); 107 | } 108 | 109 | module.exports = nconf; 110 | -------------------------------------------------------------------------------- /test/billing_service.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird').Promise; 2 | 3 | Promise.longStackTraces(); 4 | var nconf = require('../lib/config'); 5 | nconf.set('db:connection:filename', ':memory:'); 6 | 7 | var db = require('../lib/db'); 8 | 9 | var path = require('path'); 10 | var Token = require(path.join(__dirname, '/../models/token')).model; 11 | var BillingService = require(path.join(__dirname, '/../lib/billing_service')); 12 | var assert = require('assert'); 13 | 14 | describe('Billing Service', function() { 15 | var token, billing; 16 | 17 | before(function() { 18 | return db.knex.migrate.rollback(db.conf); 19 | }); 20 | 21 | beforeEach(function() { 22 | return db.knex.migrate.latest(db.conf).then(function() { 23 | billing = new BillingService(); 24 | return new Token().save().then(function(token_) { 25 | token = token_; 26 | }); 27 | }); 28 | }); 29 | 30 | afterEach(function() { 31 | return db.knex.migrate.rollback(db.conf); 32 | }); 33 | 34 | it('should credit the token with a positive balance', function() { 35 | 36 | return billing.credit(token, 250).then(function(credit) { 37 | assert.strictEqual(credit.get('amount'), 250); 38 | 39 | return token.getBalance().then(function(balance) { 40 | assert.strictEqual(balance.get('balance'), credit.get('amount')); 41 | }); 42 | }); 43 | }); 44 | 45 | it('should charge the balance of a token', function() { 46 | return billing.credit(token, 100).then(function() { 47 | return billing.debit(token, 11.235).then(function(debit) { 48 | assert.strictEqual(debit.get('amount'), 11.235); 49 | }) 50 | }); 51 | }) 52 | 53 | it('should subtract from a balance upon debit', function() { 54 | var amount; 55 | var debitAmount = 3.33; 56 | 57 | return billing.getBalance(token).then(function(balance) { 58 | amount = balance.get('balance'); 59 | 60 | return balance.debit(debitAmount).then(function(debit) { 61 | assert.strictEqual(debit.get('amount'), debitAmount); 62 | assert.strictEqual(balance.get('amount', amount - debitAmount)); 63 | }); 64 | }); 65 | }); 66 | 67 | it('should add to a balance upon credit', function() { 68 | var amount; 69 | var creditAmount = 5; 70 | 71 | return billing.getBalance(token).then(function(balance) { 72 | amount = balance.get('balance'); 73 | 74 | return balance.credit(creditAmount).then(function(credit) { 75 | assert.strictEqual(credit.get('amount'), creditAmount); 76 | 77 | return balance.refresh().then(function(balance) { 78 | assert.strictEqual(balance.get('balance'), amount + creditAmount); 79 | }); 80 | }); 81 | }); 82 | }); 83 | 84 | it('should prevent overdrafts, draining the balance', function() { 85 | var insaneAmount = 999999999999; 86 | 87 | return billing.credit(token, 100).then(function() { 88 | return billing.getBalance(token).then(function(balance) { 89 | return billing.debit(token, insaneAmount) 90 | .catch(BillingService.Overdraft, function(overdraft) { 91 | assert.strictEqual(overdraft.debit.get('amount') , balance.get('balance')); 92 | assert.strictEqual(overdraft.remainder, insaneAmount - balance.get('balance')); 93 | 94 | return balance.refresh().then(function(balance) { 95 | assert.strictEqual(balance.get('balance'), 0); 96 | }); 97 | }); 98 | }); 99 | }); 100 | }); 101 | 102 | it('should have many debits per balance', function() { 103 | return billing.credit(token, 1000).then(function() { 104 | return billing.debit(token, 100).then(function() { 105 | return billing.getDebits(token).then(function(debits) { 106 | assert(debits.length > 0); 107 | }); 108 | }); 109 | }); 110 | }); 111 | 112 | it('should have many credits per balance', function() { 113 | return billing.credit(token, 100).then(function() { 114 | return billing.getCredits(token).then(function(credits) { 115 | assert(credits.length > 0); 116 | }); 117 | }); 118 | }); 119 | }); 120 | 121 | -------------------------------------------------------------------------------- /routes/post_contract.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | 21 | var tarStream = require('tar-stream'); 22 | var zlib = require('zlib'); 23 | var concat = require('concat-stream'); 24 | var winston = require('winston'); 25 | var path = require('path'); 26 | 27 | var Contract = require('../models/contract').model; 28 | 29 | /** 30 | * Upload a contract. 31 | */ 32 | module.exports = function (req, res, next) { 33 | var fileManager = req.app.get('fileManager'); 34 | var gunzip = zlib.createGunzip(); 35 | var tarExtract = tarStream.extract(); 36 | req.pipe(gunzip).pipe(tarExtract); 37 | 38 | var fileRegistry = {}; 39 | var fakeFilesystem = { 40 | readFile: function (filename) { 41 | return fileRegistry[path.normalize(filename)].data; 42 | }, 43 | stat: function (filename) { 44 | return { 45 | isFile: function () { 46 | return !fileRegistry[path.normalize(filename)].directory; 47 | }, 48 | isDirectory: function () { 49 | return !!fileRegistry[path.normalize(filename)].directory; 50 | } 51 | } 52 | }, 53 | exists: function (filename) { 54 | return !!fileRegistry[path.normalize(filename)]; 55 | }, 56 | readdir: function (dirname) { 57 | dirname = path.normalize(dirname).replace(/\/*$/, '/'); 58 | var files = Object.keys(fileRegistry).filter(function (filename) { 59 | return filename.substr(0, dirname.length) === dirname && 60 | filename !== dirname; 61 | }).map(function (filename) { 62 | return filename.substr(dirname.length); 63 | }).filter(function (filename) { 64 | return !/\/./.exec(filename); 65 | }); 66 | return files; 67 | } 68 | }; 69 | 70 | // While parsing tar file, load all files into an in-memory array 71 | tarExtract.on('entry', function(header, stream, callback) { 72 | var filename = path.normalize(header.name); 73 | winston.debug('processing file', filename); 74 | fileRegistry[filename] = header; 75 | if (header.type !== 'file') return callback(); 76 | 77 | stream.pipe(concat(function (fileData) { 78 | if (!Buffer.isBuffer(fileData)) { 79 | fileData = new Buffer(0); 80 | } 81 | fileRegistry[filename].data = fileData; 82 | callback(); 83 | })); 84 | }); 85 | 86 | tarExtract.on('finish', function() { 87 | var compiler = req.app.get('compiler'); 88 | compiler.setFilesystem(fakeFilesystem); 89 | compiler.on('file', function (event) { 90 | fileManager.storeFileWithHash(event.hash, event.data); 91 | }); 92 | 93 | var contractHash = compiler.compileModule(''); 94 | 95 | var knex = req.app.get('knex'); 96 | new Contract({hash: contractHash}).fetch().then(function (contract) { 97 | if (contract) { 98 | return contract; 99 | } else { 100 | return Contract.forge({ 101 | hash: contractHash 102 | }).save(); 103 | } 104 | }).then(function (contract) { 105 | winston.debug('stored contract', contract.get('hash')); 106 | res.status(200).json({ 107 | hash: contract.get('hash'), 108 | expires: contract.get('expires') 109 | }); 110 | }).catch(function (error) { 111 | next(error); 112 | }); 113 | }); 114 | }; 115 | -------------------------------------------------------------------------------- /processes/ripple_billing.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | module.exports = function(codius) { 20 | 21 | if (codius.features.isEnabled('RIPPLE_BILLING')) { 22 | codius.logger.info("Ripple Billing Feature Enabled") 23 | 24 | const RippleAccountMonitor = require('ripple-account-monitor') 25 | const http = require('superagent') 26 | const Promise = require('bluebird') 27 | const RIPPLE_REST_URL = 'https://api.ripple.com/' 28 | const ADDRESS = codius.config.get('RIPPLE_ADDRESS') 29 | const billing = new codius.BillingService() 30 | const CPU_PER_XRP = codius.config.get('compute_units_per_xrp') 31 | const CPU_PER_BITCOIN = codius.config.get('compute_units_per_bitcoin') 32 | 33 | if (!ADDRESS) { 34 | throw new Error('RIPPLE_ADDRESS must be set in environment to enable Ripple billing') 35 | } 36 | 37 | codius.Ledger.findOrCreate({ name: 'ripple' }).then(function(ledger) { 38 | 39 | codius.events.on('contract:created', function(token) { 40 | ledger.registerAddress(token, ADDRESS+'?dt='+token.get('id')) 41 | .then(function(address) { 42 | codius.logger.info('address:registered', address.get('address')) 43 | }) 44 | }) 45 | 46 | fetchLastHash(ledger, ADDRESS).then(function(hash) { 47 | 48 | ledger.set('last_hash', hash).save().then(function() { 49 | var monitor = new RippleAccountMonitor({ 50 | rippleRestUrl: RIPPLE_REST_URL, 51 | account: ADDRESS, 52 | lastHash: hash, 53 | timeout: 3000, 54 | onTransaction: function(transaction, next) { 55 | ledger.set('last_hash', transaction.hash).save().then(next) 56 | }, 57 | onPayment: function(payment, next) { 58 | handlePayment(payment).then(function() { 59 | ledger.set('last_hash', payment.hash).save().then(next) 60 | }) 61 | } 62 | }) 63 | 64 | monitor.start() 65 | codius.logger.info('Starting Ripple Monitor') 66 | }) 67 | }) 68 | }) 69 | 70 | function handlePayment(payment, next) { 71 | return new Promise(function(resolve, reject) { 72 | if (payment.DestinationTag) { 73 | var CPU 74 | if (!payment.Amount.currency) { // XRP (in drops) 75 | CPU = payment.Amount / 1000000 * CPU_PER_XRP 76 | } else if (payment.Amount.currency === 'BTC') { 77 | CPU = payment.Amount.value * CPU_PER_BITCOIN 78 | } 79 | new codius.Token({ id: payment.DestinationTag }).fetch().then(function(token) { 80 | billing.credit(token, CPU).then(function() { 81 | codius.logger.info('token:credited', token.get('token'), CPU) 82 | resolve() 83 | }) 84 | }) 85 | } else { 86 | resolve() 87 | } 88 | }) 89 | } 90 | 91 | function fetchLastHash(ledger, account) { 92 | return new Promise(function(resolve, reject) { 93 | if (ledger.get('last_hash')) { 94 | resolve(ledger.get('last_hash')) 95 | } else { 96 | http 97 | .get(RIPPLE_REST_URL+'v1/accounts/'+account+'/payments') 98 | .end(function(error, response) { 99 | if (error) { return reject(error) } 100 | if (!response.body.success) { return reject(new Error(response.body)) } 101 | resolve(response.body.payments[0].hash) 102 | }) 103 | } 104 | }) 105 | } 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /test/compute_service.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird').Promise; 2 | 3 | Promise.longStackTraces(); 4 | var nconf = require('../lib/config'); 5 | nconf.set('db:connection:filename', ':memory:'); 6 | 7 | var db = require('../lib/db'); 8 | 9 | var assert = require('assert') 10 | var _ = require('lodash') 11 | var path = require('path') 12 | var uuid = require('uuid') 13 | var compute = require(path.join(__dirname, '/../lib/compute_service')) 14 | var engine = require(path.join(__dirname, '/../lib/engine')) 15 | var Contract = require(path.join(__dirname, '/../models/contract')).model 16 | var Token = require(path.join(__dirname, '/../models/token')).model 17 | var chai = require('chai'); 18 | var chaiAsPromised = require('chai-as-promised'); 19 | var temp = require('temp'); 20 | 21 | temp.track(); 22 | 23 | chai.use(chaiAsPromised); 24 | var expect = chai.expect; 25 | 26 | describe('Compute Service', function() { 27 | var contract, token, contractHash; 28 | 29 | before(function() { 30 | return db.knex.migrate.rollback(db.conf); 31 | }); 32 | 33 | beforeEach(function() { 34 | return Promise.promisify(temp.mkdir)('codius-host-test').then(function(dir) { 35 | engine.engineConfig.contractsFilesystemPath = dir; 36 | var fileManager = engine.fileManager; 37 | var compiler = engine.compiler; 38 | var currentDir = path.join(__dirname, '/test_contract'); 39 | 40 | return db.knex.migrate.latest(db.conf).then(function () { 41 | var p = []; 42 | compiler.on('file', function (event) { 43 | if (event.name.indexOf(currentDir) !== 0) { 44 | throw new Error('File path does not have current directory prefix: ' + event.name); 45 | } 46 | p.push(fileManager.storeFileWithHash(event.hash, event.data)); 47 | }); 48 | 49 | contractHash = compiler.compileModule(currentDir); 50 | 51 | p.push(new Contract({hash: contractHash}).fetch().then(function (_contract) { 52 | if (_contract) { 53 | return _contract; 54 | } else { 55 | return Contract.forge({ 56 | hash: contractHash 57 | }).save(); 58 | } 59 | }).then(function (_contract) { 60 | contract = _contract; 61 | return new Token({ token: uuid.v4(), contract_id: contract.get('id')}).save().then(function(token_) { 62 | token = token_; 63 | compute._instances[token.get('token')] = { 64 | state: 'pending', 65 | container_hash: contract.get('hash') 66 | } 67 | }); 68 | }) 69 | ); 70 | 71 | return Promise.all(p); 72 | }); 73 | }); 74 | }); 75 | 76 | afterEach(function() { 77 | return db.knex.migrate.rollback(db.conf); 78 | }); 79 | 80 | it('should start a new running instance', function(done) { 81 | 82 | expect(compute.startInstance(token).then(function(instance) { 83 | assert.strictEqual(instance.token, token.get('token')); 84 | assert.strictEqual(instance.container_hash, contractHash); 85 | assert.strictEqual(instance.state, 'running'); 86 | })).to.notify(done); 87 | }); 88 | 89 | it('should list all instances', function() { 90 | 91 | return compute.startInstance(token).then(function(instance) { 92 | return compute.getInstances().then(function(instances) { 93 | var idx = _.findIndex(instances, function(instance) { return instance.token === token.get('token'); }); 94 | assert.notStrictEqual(idx, -1) 95 | assert.strictEqual(instances[idx].container_hash, contractHash); 96 | assert.strictEqual(instances[idx].state, 'running'); 97 | }); 98 | }); 99 | }) 100 | 101 | it('should get info on single running instance', function() { 102 | return compute.startInstance(token).then(function(_) { 103 | return compute.getInstance(token).then(function(instance) { 104 | assert.strictEqual(instance.token, token.get('token')); 105 | assert.strictEqual(instance.container_hash, contractHash); 106 | assert.strictEqual(instance.state, 'running'); 107 | }); 108 | }); 109 | }); 110 | 111 | it('should stop a running instance', function() { 112 | return compute.startInstance(token).then(function(instance) { 113 | return compute.stopInstance(token); 114 | }).then(function(state) { 115 | assert.strictEqual(state, 'stopping'); 116 | }); 117 | }); 118 | 119 | it.skip('should get a quote to run an instance', function(done) { 120 | 121 | expect(compute.getQuote().then(function(quote) { 122 | assert(quote); 123 | })).to.notify(done); 124 | }); 125 | }); 126 | 127 | -------------------------------------------------------------------------------- /test/http/compute_service.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird').Promise; 2 | 3 | Promise.longStackTraces(); 4 | 5 | var assert = require('assert') 6 | var _ = require('lodash') 7 | var path = require('path') 8 | var supertest = require('supertest-as-promised') 9 | var uuid = require('uuid') 10 | var codius = require(path.join(__dirname, '/../../')) 11 | var engine = require(path.join(__dirname, '/../../lib/engine')) 12 | var Server = require(path.join(__dirname, '/../../compute-service/server')); 13 | var Contract = require(path.join(__dirname, '/../../models/contract')).model 14 | var temp = require('temp'); 15 | 16 | temp.track(); 17 | 18 | describe('Compute Service HTTP Interface', function() { 19 | var contract, token, contractHash, server, http; 20 | 21 | before(function() { 22 | return Promise.promisify(temp.mkdir)('codius-host-test').then(function(dir) { 23 | engine.engineConfig.contractsFilesystemPath = dir; 24 | var fileManager = engine.fileManager; 25 | var compiler = engine.compiler; 26 | var currentDir = path.join(__dirname, '/../test_contract'); 27 | var p = []; 28 | 29 | compiler.on('file', function (event) { 30 | if (event.name.indexOf(currentDir) !== 0) { 31 | throw new Error('File path does not have current directory prefix: ' + event.name); 32 | } 33 | p.push(fileManager.storeFileWithHash(event.hash, event.data)); 34 | }); 35 | 36 | contractHash = compiler.compileModule(currentDir); 37 | 38 | p.push(new Contract({hash: contractHash}).fetch() 39 | .then(function (_contract) { 40 | if (_contract) { 41 | return _contract; 42 | } else { 43 | return Contract.forge({ 44 | hash: contractHash 45 | }).save(); 46 | } 47 | }).then(function (_contract) { 48 | contract = _contract; 49 | return new codius.Token({ token: uuid.v4(), contract_id: contract.get('id')}).save() 50 | .then(function(token_) { 51 | token = token_; 52 | codius.compute._instances[token.get('token')] = { 53 | state: 'pending', 54 | container_hash: contract.get('hash') 55 | } 56 | }); 57 | }) 58 | ); 59 | 60 | return Promise.all(p); 61 | }); 62 | }); 63 | 64 | beforeEach(function() { 65 | server = Server(codius); 66 | http = supertest(server); 67 | }); 68 | 69 | after(function() { 70 | // TODO: Remove contract from filesystem 71 | return contract.destroy().then(function(){ 72 | return token.destroy(); 73 | }); 74 | }); 75 | 76 | it('should start running a container', function() { 77 | return http 78 | .post('/instances') 79 | .send({ 80 | token: token.get('token'), 81 | // container_uri: 82 | // type: 83 | // vars: 84 | // port: 85 | }) 86 | .expect(200) 87 | .then(function(response) { 88 | assert.strictEqual(response.body.instance.token, token.get('token')) 89 | assert.strictEqual(response.body.instance.container_hash, contractHash) 90 | assert.strictEqual(response.body.instance.state, 'running') 91 | }); 92 | }) 93 | 94 | it('should list all running containers', function() { 95 | return http 96 | .get('/instances') 97 | .expect(200) 98 | .then(function(response) { 99 | var idx = _.findIndex(response.body.instances, function(instance) { return instance.token === token.get('token'); }); 100 | assert.notStrictEqual(idx, -1) 101 | assert.strictEqual(response.body.instances[idx].container_hash, contractHash) 102 | assert.strictEqual(response.body.instances[idx].state, 'running') 103 | }); 104 | }) 105 | 106 | it('should get info on single running container', function() { 107 | return http 108 | .get('/instances/'+token.get('token')) 109 | .expect(200) 110 | .then(function(response) { 111 | assert.strictEqual(response.body.instance.token, token.get('token')) 112 | assert.strictEqual(response.body.instance.container_hash, contractHash) 113 | assert.strictEqual(response.body.instance.state, 'running') 114 | }); 115 | }) 116 | 117 | it('should stop a running container', function() { 118 | return http 119 | .delete('/instances/'+token.get('token')) 120 | .expect(200) 121 | .then(function(response) { 122 | assert.strictEqual(response.body.instance.state, 'stopping') 123 | }); 124 | }) 125 | 126 | it.skip('should get a quote to run a container', function() { 127 | return http 128 | .get('/quote') 129 | .expect(200); 130 | }) 131 | 132 | }) 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/codius/codius-host.svg?branch=master)](https://travis-ci.org/codius/codius-host) 2 | # Codius host 3 | 4 | **NOT READY FOR PRODUCTION USE** 5 | 6 | This is a prototype implementation of a Codius host. Codius hosts run applications and provide them with APIs that allow for key generation, interaction with outside systems and lots more! Please keep in mind that is an early prototype and it's still missing a lot of functionality for it to be secure, stable and useful. 7 | 8 | ## Prerequisites 9 | 10 | To use the Codius host, you need a recent version of Linux. 11 | 12 | ### Linux 13 | 14 | For the example commands below, we assume you're on Ubuntu 14.04 or later. But most up-to-date Linux distributions should work. We definitely recommend being on the latest stable release though. 15 | 16 | If you're on Windows/Mac try installing [Vagrant](https://docs.vagrantup.com/v2/installation/index.html) and then run: 17 | 18 | ```sh 19 | vagrant init ubuntu/trusty32 20 | vagrant up 21 | vagrant ssh 22 | ``` 23 | 24 | Congratulations, you are running Ubuntu/Linux! Proceed. 25 | 26 | ### 32-bit libc/libstdc++ (Skip if you're using Vagrant or a 32-bit installation) 27 | 28 | On 64-bit systems you need to have the 32-bit versions of libc, libstdc++ and libseccomp installed. 29 | 30 | On Ubuntu, run: 31 | 32 | ``` sh 33 | sudo dpkg --add-architecture i386 34 | sudo apt-get update 35 | sudo apt-get install libc6-i386 lib32stdc++6 libseccomp2:i386 36 | ``` 37 | 38 | ### git 39 | 40 | Install git by running: 41 | 42 | ``` sh 43 | sudo apt-get install git 44 | ``` 45 | 46 | ### Node.js 47 | 48 | Next, you need a recent version of Node.js. All versions of 0.10.x or higher should work. 49 | 50 | On Ubuntu, you can install Node.js simply by: 51 | 52 | ```sh 53 | sudo add-apt-repository ppa:chris-lea/node.js 54 | sudo apt-get update 55 | sudo apt-get install nodejs 56 | sudo ln -s /usr/bin/nodejs /usr/local/bin/node 57 | ``` 58 | 59 | ### sqlite3 60 | 61 | Install sqlite3 by running: 62 | 63 | ``` sh 64 | sudo npm install -g sqlite3 65 | ``` 66 | 67 | ## Installation 68 | 69 | ``` sh 70 | sudo npm install -g codius-host 71 | ``` 72 | 73 | ## Getting started 74 | 75 | To interact with your Codius host, check out the [Codius CLI](https://www.npmjs.com/package/codius). 76 | 77 | ### Setting up a local Codius host for testing 78 | 79 | #### Certificate 80 | 81 | First, you need to generate a self-signed certificate. Run the following OpenSSL command to generate RSA keys and note where your server.key and server.crt are located: 82 | 83 | ``` sh 84 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt 85 | ``` 86 | 87 | #### Local hostname 88 | 89 | In order to use a local Codius host, you need to redirect requests like https://abcabc-abcabc-abcabc.example.com to your local host. Unfortunately, `/etc/hosts` does not allow you to specify wildcard hosts. 90 | 91 | On Ubuntu, an easy way to get around this problem is using `dnsmasq`. 92 | 93 | ``` sh 94 | sudo apt-get install dnsmasq 95 | echo 'address=/localcodius/127.0.0.1' | sudo tee --append /etc/dnsmasq.conf 96 | sudo /etc/init.d/dnsmasq restart 97 | ``` 98 | 99 | Afterwards, configure your Codius host to use "localcodius" as its hostname. You'll be able to access local applications using URLs like https://abcabc-abcabc-abcabc.localcodius:2633. 100 | 101 | #### Run 102 | 103 | ``` sh 104 | SSL_CERT=/path/to/server.crt SSL_KEY=/path/to/server.key codius-host start 105 | ``` 106 | 107 | ### Configuration options 108 | 109 | #### Starting balance 110 | 111 | By default, an application is considered **pending** when uploaded to your Codius host until its balance is credited. However, you can choose to give applications a free starting balance by modifying `starting_cpu_balance` in `/lib/config.js` or running with: 112 | 113 | ``` sh 114 | starting_cpu_balance=100 codius-host start 115 | ``` 116 | 117 | #### Billing 118 | 119 | Codius host can be run with Bitcoin (alongside bitcoind) and/or Ripple billing enabled like so: 120 | 121 | ``` sh 122 | BITCOIND_HOST=your_bitcoind BITCOIND_PORT=bitcoind_port BITCOIND_USER=Your_Username BITCOIND_PASS=Your_Password codius-host start -f bitcoin_billing 123 | ``` 124 | 125 | ``` sh 126 | RIPPLE_ADDRESS=rYOURRIPPLEADDRESS codius-host start -f ripple_billing 127 | ``` 128 | 129 | See `/lib/config.js` to modify values such as `cpu_per_bitcoin`. 130 | 131 | ## Contributing 132 | 133 | Development of features should be made on the `master` branch behind a Feature Flag. To create a feaure flag require `lib/features.js` and only run your feature's code if the feature is enabled. Feature names are in ALL_CAPS. 134 | 135 | ```` 136 | var features = require('lib/features') 137 | 138 | if (features.isEnabled('MY_COOL_FEATURE')) { 139 | // New code belongs here 140 | } 141 | ```` 142 | 143 | Features are enabled at startup using the command line flag -f or --features. Multiple features can be specified using commas without spaces. 144 | 145 | ```` 146 | codius-host start --features my_cool_feature 147 | 148 | codius-host start -f feature_one,feature_two 149 | ```` 150 | 151 | ## Docker Container 152 | 153 | codius-host is also released as a docker container. You can pull it down via: 154 | 155 | ``` 156 | docker pull codius/codius-host 157 | ``` 158 | 159 | And it can be started with: 160 | 161 | ``` 162 | docker run codius/codius-host --help 163 | ``` 164 | 165 | The docker container uses two environment variables: 166 | 167 | * PORT - The port that the container will listen on within the container. Defaults to 8080. 168 | * CONTRACTS_STORAGE - Location within the container that contracts will be 169 | saved. Defaults to /contracts/. You can mount a local filesystem on top of it 170 | by passing ```-v /path/to/storage:/contracts``` to your ```docker run``` command. 171 | -------------------------------------------------------------------------------- /lib/amortizer.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of Codius: https://github.com/codius 4 | Copyright (c) 2014 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | var path = require('path'); 21 | var Promise = require('bluebird'); 22 | var request = require('request'); 23 | var _ = require('underscore'); 24 | var winston = require('winston'); 25 | 26 | var BillingService = require(path.join(__dirname, 'billing_service')); 27 | var config = require(path.join(__dirname, 'config')); 28 | var events = require(path.join(__dirname, 'events')); 29 | var Token = require(path.join(__dirname, '/../models/token')).model; 30 | 31 | /** 32 | * Class that bills tokens for their running time 33 | * 34 | * @param {Integer} opts.pollInterval 35 | * @param {Integer} opts.millisecondsPerComputeUnit 36 | */ 37 | function Amortizer () { 38 | var self = this; 39 | 40 | self._instances = {}; 41 | self._millisecondsPerComputeUnit = config.get('milliseconds_per_compute_unit'); 42 | self._polling = false; 43 | self._poll = null; 44 | 45 | /** 46 | * Update instance's balance. 47 | */ 48 | function updateBalance(balance) { 49 | new Token({id: balance.get('token_id')}).fetch() 50 | .then(function(token) { 51 | if (token && token.get('token') in self._instances) { 52 | self._instances[token.get('token')].lastCheckedBalance = balance.get('balance'); 53 | } 54 | }) 55 | } 56 | 57 | events.on('contract:started', function(token) { 58 | if (token.get('token') in self._instances) { 59 | winston.debug('Token balance already being amortized: ' + token.get('token')); 60 | return; 61 | } 62 | new BillingService().getBalance(token) 63 | .then(function(balance) { 64 | if (!balance) { 65 | throw new Error('Invalid balance token:', token.get('token')); 66 | } 67 | winston.info('Starting to amortize token balance:', token.get('token')); 68 | self._instances[token.get('token')] = { 69 | lastChargedTime: Date.now(), 70 | lastCheckedBalance: balance.get('balance'), 71 | charging: false 72 | }; 73 | }) 74 | }) 75 | events.on('contract:stopped', function(token) { 76 | delete self._instances[token]; 77 | }); 78 | events.on('balance:credited', updateBalance); 79 | events.on('balance:debited', updateBalance); 80 | } 81 | 82 | 83 | /** 84 | * Start the recursive pollRunningInstances loop 85 | */ 86 | Amortizer.prototype.startPollingRunningInstances = function(pollInterval){ 87 | var self = this; 88 | 89 | if (self._polling) { 90 | winston.info('Amortizer is already polling'); 91 | return; 92 | } 93 | if (!pollInterval) { 94 | pollInterval = 100 95 | } 96 | self._polling = true; 97 | // TODO: Should minBalance just be 0? Instances are terminating with a balance of 1. 98 | var minBalance = Math.ceil(pollInterval / self._millisecondsPerComputeUnit); 99 | self._poll = setInterval(function(){ 100 | self.pollRunningInstances(minBalance); 101 | }, pollInterval); 102 | }; 103 | 104 | /** 105 | * Check each of the active tokens to determine if their balances are 106 | * running low. If one appears to have a balance less than the minimum, 107 | * charge the token for its time used. If the balance is still too low, 108 | * then kill the instance. 109 | */ 110 | Amortizer.prototype.pollRunningInstances = function(minBalance){ 111 | var self = this; 112 | 113 | request 114 | .get('http://127.0.0.1:' + ((parseInt(process.env.PORT) || 5000) + 30) + '/instances', function(err, res, body){ 115 | var activeInstances; 116 | if (!err && res.statusCode == 200) { 117 | activeInstances = JSON.parse(body).instances 118 | } else { 119 | winston.debug('Error polling instances: ' + err); 120 | activeInstances = []; 121 | } 122 | return Promise.each(activeInstances, function(activeInstance){ 123 | new Token({token: activeInstance.token}).fetch().then(function(token) { 124 | if (!token) { 125 | winston.debug('Error: invalid token (' + activeInstance.token + ')'); 126 | return; 127 | } 128 | // Check balance of running instances in the _instances array 129 | // that are not in the process of being charged 130 | // Instances are added when their balances are first credited 131 | if (activeInstance.state!=='running' || !(token.get('token') in self._instances) || 132 | self._instances[token.get('token')].charging) { 133 | return; 134 | } 135 | // Kill instances with depleted balance 136 | // Note that we don't check the balance every time 137 | // to avoid unnecessary database reads 138 | if (self._instances[token.get('token')].lastCheckedBalance <= minBalance) { 139 | delete self._instances[token.get('token')]; 140 | request.del('http://127.0.0.1:'+((parseInt(process.env.PORT) || 5000) + 30)+ 141 | '/instances/'+token.get('token'), function (error, res, body) { 142 | if (error) { 143 | winston.debug('Error killing instance (' + token.get('token') + '): ' + error); 144 | } 145 | }); 146 | } else if (self._instances[token.get('token')].lastCheckedBalance - self.calculateCharge(token) <= minBalance) { 147 | self.chargeToken(token); 148 | } 149 | }) 150 | }) 151 | }); 152 | }; 153 | 154 | /** 155 | * Charge is based on the cost per millisecond, which is set by the host 156 | */ 157 | Amortizer.prototype.calculateCharge = function(token) { 158 | var self = this; 159 | var runTime = Date.now() - self._instances[token.get('token')].lastChargedTime; 160 | var charge = Math.ceil(runTime / self._millisecondsPerComputeUnit); 161 | return charge; 162 | }; 163 | 164 | /** 165 | * Calculate the amount owed since the lastChargedTime and apply the charge 166 | */ 167 | Amortizer.prototype.chargeToken = function(token) { 168 | var self = this; 169 | 170 | self._instances[token.get('token')].charging = true; 171 | return new BillingService().debit(token, self.calculateCharge(token)) 172 | .then(function(debit) { 173 | if (debit) { 174 | self._instances[token.get('token')].lastChargedTime = Date.now(); 175 | } 176 | self._instances[token.get('token')].charging = false; 177 | return token.getBalance(); 178 | }) 179 | .then(function(balance) { 180 | return balance.get('balance') 181 | }) 182 | }; 183 | 184 | /** 185 | * If the token has an instance currently running 186 | * we need to charge it to be able to return the most current 187 | * balance, otherwise we can just read the balance 188 | * from the database 189 | */ 190 | Amortizer.prototype.checkTokenBalance = function(token) { 191 | var self = this; 192 | 193 | if (token.get('token') in self._instances) { 194 | return self.chargeToken(token); 195 | } else { 196 | return token.getBalance().then(function(balance) { 197 | return balance.get('balance'); 198 | }); 199 | } 200 | }; 201 | 202 | Amortizer.prototype.stopPollingRunningInstances = function(){ 203 | var self = this; 204 | self._polling = false; 205 | if (self._poll) { 206 | clearInterval(self._poll); 207 | self._poll = null; 208 | } 209 | }; 210 | 211 | module.exports = new Amortizer(); 212 | -------------------------------------------------------------------------------- /lib/compute_service.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var Promise = require('bluebird'); 4 | var winston = require('winston'); 5 | var chalk = require('chalk'); 6 | var _ = require('underscore'); 7 | 8 | Promise.promisifyAll(fs); 9 | 10 | var formatter = require(path.join(__dirname, 'formatter')); 11 | var engine = require(path.join(__dirname, 'engine')); 12 | var config = require(path.join(__dirname, 'config')); 13 | var events = require(path.join(__dirname, 'events')); 14 | var Contract = require(path.join(__dirname, '/../models/contract')).model; 15 | var Token = require(path.join(__dirname, '/../models/token')).model; 16 | 17 | /** 18 | * Class that manages starting and stoping instances 19 | * 20 | */ 21 | function ComputeService () { 22 | var self = this; 23 | 24 | self._uniqueRunId = 0; 25 | self._instances = {}; // instance data 26 | } 27 | 28 | ComputeService.prototype.startInstance = function(token, container_uri, type, vars, port) { 29 | var this_ = this; 30 | return new Promise(function(resolve, reject) { 31 | ensureToken(token); 32 | var runId = this_._uniqueRunId++; 33 | 34 | var contractToken = token.get('token'); 35 | if (!(contractToken in this_._instances)) { 36 | return reject(new Error('Invalid contract token', contractToken)); 37 | } else if (this_._instances[contractToken].state!=='pending') { 38 | winston.debug(contractIdent, chalk.dim('+++'), 'Contract is already started'); 39 | return reject(new Error('Instance already started')); 40 | } 41 | 42 | var contractHash = this_._instances[contractToken].container_hash; 43 | 44 | var contractIdent = formatter.hash(contractHash) + chalk.dim(':' + runId); 45 | 46 | winston.debug(contractIdent, chalk.dim('+++'), 'Starting instance'); 47 | 48 | var runner = engine.engine.runContract(contractHash); 49 | 50 | // TODO: modify the engine and sandbox to make this work 51 | // runner._sandbox.pipeStdout({ 52 | // write: function (output) { 53 | // // TODO Redirect to a stream that clients can subscribe to 54 | // winston.debug(contractIdent, chalk.dim('...'),output.replace(/\n$/, '')); 55 | // } 56 | // }); 57 | this_._instances[contractToken].runner = runner; 58 | this_._instances[contractToken].type = type; 59 | this_._instances[contractToken].state = 'running'; 60 | this_._instances[contractToken].port = port; 61 | this_._instances[contractToken].container_uri = container_uri; 62 | // TODO: Update engine to assign ip address to instance. 63 | // this_._instances[contractToken].ip_address = ip_address; 64 | 65 | events.emit('contract:started', token); 66 | // TODO: Emit 'running' from codius-engine 67 | // runner.on('running', function() { 68 | // this_._instances[contractToken].state = 'running' 69 | // }); 70 | 71 | runner.on('exit', function (code, signal) { 72 | winston.debug(contractIdent, chalk.dim('+++'), 'Instance stopped'); 73 | this_._instances[contractToken].state = 'stopped'; 74 | events.emit('contract:stopped', token); 75 | 76 | // Delete the contract from the filesystem 77 | // TODO: Update codius-engine to handle contract deletion 78 | // engine.engine.deleteContract(contractHash); 79 | function deleteFileByHash(hash) { 80 | var firstDir = path.join(engine.engineConfig.contractsFilesystemPath,hash.slice(0, 2)); 81 | var secondDir = path.join(firstDir, hash.slice(2, 4)); 82 | var filePath = path.join(secondDir, hash); 83 | return fs.unlinkAsync(filePath).then(function() { 84 | // Delete firstDir & secondDir if empty 85 | return fs.readdirAsync(secondDir); 86 | }).then(function(files) { 87 | if (files.length===0) { 88 | return fs.rmdirAsync(secondDir); 89 | } 90 | }).then(function() { 91 | return fs.readdirAsync(firstDir); 92 | }).then(function(files) { 93 | if (files.length===0) { 94 | return fs.rmdir(firstDir); 95 | } 96 | }); 97 | }; 98 | function deleteFile(file) { 99 | // Files in subdirectories are stored in the manifest as: 100 | // {subdir_name: {file_name: HASH}} 101 | if (typeof file==='object') { 102 | var p = []; 103 | _.each(file, function(subfile) { 104 | p.push(deleteFile(subfile)); 105 | }); 106 | return Promise.all(p); 107 | } else { 108 | return deleteFileByHash(file) 109 | } 110 | } 111 | var manifest = runner.getManifest(contractHash); 112 | var p = []; 113 | 114 | // TODO: Check if files are used in other running applications before deleting. 115 | // _.each(manifest.files, function(file) { 116 | // p.push(deleteFile(file)) 117 | // }); 118 | // p.push(deleteFile(contractHash)); 119 | 120 | Promise.all(p).then(function() { 121 | winston.debug(contractIdent, chalk.dim('+++'), 'Instance terminated'); 122 | this_._instances[contractToken].state = 'terminated'; 123 | }); 124 | }); 125 | 126 | return resolve(formatInstance(this_._instances[contractToken], contractToken)); 127 | }); 128 | } 129 | 130 | 131 | ComputeService.prototype.getInstances = function() { 132 | var this_ = this; 133 | //Should this only return running instances? 134 | return Promise.resolve(_.map(this_._instances, function(instance, token){ 135 | return formatInstance(instance, token); 136 | })); 137 | } 138 | 139 | ComputeService.prototype.getInstance = function(token) { 140 | var this_ = this; 141 | return new Promise(function(resolve, reject) { 142 | if (!token) { reject(new Error('token must be provided')) } 143 | var instance = this_._instances[token.get('token')]; 144 | if (!instance) { 145 | reject (new Error('Invalid instance token')); 146 | } 147 | return resolve(formatInstance(instance, token.get('token'))); 148 | }) 149 | } 150 | 151 | ComputeService.prototype.stopInstance = function(token) { 152 | var this_ = this; 153 | if (!token) { Promise.reject(new Error('token must be provided')) } 154 | var instance = this_._instances[token.get('token')]; 155 | if (!instance || !instance.runner) { 156 | return Promise.reject (new Error('Invalid instance token')); 157 | } 158 | if (instance.state!=='running') { 159 | return Promise.reject (new Error('Cannot kill non-running instance')); 160 | } 161 | instance.state = 'stopping'; 162 | instance.runner.kill(); 163 | return Promise.resolve(instance.state); 164 | } 165 | 166 | /** 167 | * Pass the incoming stream to an existing contract instance 168 | */ 169 | ComputeService.prototype.handleConnection = function (token, stream) { 170 | var self = this; 171 | 172 | // TODO: handle the error created when the stream is closed 173 | // because the contract is killed due to a low balance 174 | stream.on('error', function(error){ 175 | winston.debug(contractIdent, chalk.dim('+++'), 'Stream error: ' + error.message); 176 | }); 177 | 178 | new Token({token: token}).fetch({withRelated: ['contract']}).then(function (model) { 179 | if (!model) { 180 | // TODO: Handle error somehow 181 | winston.debug(contractIdent, chalk.dim('+++'), 'Stream error: token (' + token + ') not found '); 182 | } else { 183 | var contractHash = model.related('contract').get('hash'); 184 | var contractToken = model.get('token'); 185 | 186 | var contractIdent = formatter.hash(contractHash); 187 | 188 | winston.debug(contractIdent, chalk.dim('+++'), 'Incoming connection'); 189 | 190 | var runner; 191 | if (contractToken in self._instances) { 192 | runner = self._instances[contractToken].runner; 193 | } 194 | if (!runner) { 195 | winston.debug(contractIdent, chalk.dim('+++'), 'Stream error: contract not found'); 196 | return; 197 | } 198 | 199 | var listener; 200 | if (listener = runner.getPortListener(engine.engineConfig.virtual_port)) { 201 | listener(stream); 202 | } else { 203 | function handleListener(event) { 204 | if (event.port !== engine.engineConfig.virtual_port) return; 205 | 206 | runner.removeListener('portListener', handleListener); 207 | 208 | // Pass socket stream to contract 209 | event.listener(stream); 210 | } 211 | runner.on('portListener', handleListener); 212 | } 213 | 214 | // TODO: Why does this not get triggered? 215 | stream.on('end', function () { 216 | winston.debug(contractIdent, chalk.dim('---'), 'Connection ended'); 217 | }); 218 | } 219 | }); 220 | }; 221 | 222 | function ensureToken(token) { 223 | if (!token) { throw new Error('token must be provided') } 224 | } 225 | 226 | // Return the instance minus the contract runner 227 | function formatInstance(instance, token) { 228 | return _.extend(_.omit(instance, 'runner'), {token:token}); 229 | } 230 | 231 | module.exports = new ComputeService(); 232 | --------------------------------------------------------------------------------