UNPKG

blockstack

Version:

The Blockstack Javascript library for authentication, identity, and storage.

683 lines (566 loc) 24.7 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.BlockstackNamespace = undefined; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); exports.makePreorderSkeleton = makePreorderSkeleton; exports.makeRegisterSkeleton = makeRegisterSkeleton; exports.makeRenewalSkeleton = makeRenewalSkeleton; exports.makeTransferSkeleton = makeTransferSkeleton; exports.makeUpdateSkeleton = makeUpdateSkeleton; exports.makeRevokeSkeleton = makeRevokeSkeleton; exports.makeNamespacePreorderSkeleton = makeNamespacePreorderSkeleton; exports.makeNamespaceRevealSkeleton = makeNamespaceRevealSkeleton; exports.makeNamespaceReadySkeleton = makeNamespaceReadySkeleton; exports.makeNameImportSkeleton = makeNameImportSkeleton; exports.makeAnnounceSkeleton = makeAnnounceSkeleton; exports.makeTokenTransferSkeleton = makeTokenTransferSkeleton; var _bitcoinjsLib = require('bitcoinjs-lib'); var _bitcoinjsLib2 = _interopRequireDefault(_bitcoinjsLib); var _bigi = require('bigi'); var _bigi2 = _interopRequireDefault(_bigi); var _utils = require('./utils'); var _config = require('../config'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } // todo : add name length / character verification // support v1 and v2 price API endpoint return values var BlockstackNamespace = exports.BlockstackNamespace = function () { function BlockstackNamespace(namespaceID) { _classCallCheck(this, BlockstackNamespace); if (namespaceID.length > 19) { throw new Error('Namespace ID too long (19 chars max)'); } if (!namespaceID.match('[0123456789abcdefghijklmnopqrstuvwxyz_-]+')) { throw new Error('Namespace ID can only use characters 0123456789abcdefghijklmnopqrstuvwxyz-_'); } this.namespaceID = namespaceID; this.version = -1; this.lifetime = -1; this.coeff = -1; this.base = -1; this.buckets = [-1]; this.nonalphaDiscount = -1; this.noVowelDiscount = -1; } _createClass(BlockstackNamespace, [{ key: 'check', value: function check() { try { this.setVersion(this.version); this.setLifetime(this.lifetime); this.setCoeff(this.coeff); this.setBase(this.base); this.setBuckets(this.buckets); this.setNonalphaDiscount(this.nonalphaDiscount); this.setNoVowelDiscount(this.noVowelDiscount); return true; } catch (e) { return false; } } }, { key: 'setVersion', value: function setVersion(version) { if (version < 0 || version > Math.pow(2, 16) - 1) { throw new Error('Invalid version: must be a 16-bit number'); } this.version = version; } }, { key: 'setLifetime', value: function setLifetime(lifetime) { if (lifetime < 0 || lifetime > Math.pow(2, 32) - 1) { throw new Error('Invalid lifetime: must be a 32-bit number'); } this.lifetime = lifetime; } }, { key: 'setCoeff', value: function setCoeff(coeff) { if (coeff < 0 || coeff > 255) { throw new Error('Invalid coeff: must be an 8-bit number'); } this.coeff = coeff; } }, { key: 'setBase', value: function setBase(base) { if (base < 0 || base > 255) { throw new Error('Invalid base: must be an 8-bit number'); } this.base = base; } }, { key: 'setBuckets', value: function setBuckets(buckets) { if (buckets.length !== 16) { throw new Error('Invalid buckets: must have 16 entries'); } for (var i = 0; i < buckets.length; i++) { if (buckets[i] < 0 || buckets[i] > 15) { throw new Error('Invalid buckets: must be 4-bit numbers'); } } this.buckets = buckets.slice(0); } }, { key: 'setNonalphaDiscount', value: function setNonalphaDiscount(nonalphaDiscount) { if (nonalphaDiscount <= 0 || nonalphaDiscount > 15) { throw new Error('Invalid nonalphaDiscount: must be a positive 4-bit number'); } this.nonalphaDiscount = nonalphaDiscount; } }, { key: 'setNoVowelDiscount', value: function setNoVowelDiscount(noVowelDiscount) { if (noVowelDiscount <= 0 || noVowelDiscount > 15) { throw new Error('Invalid noVowelDiscount: must be a positive 4-bit number'); } this.noVowelDiscount = noVowelDiscount; } }, { key: 'toHexPayload', value: function toHexPayload() { var lifeHex = ('00000000' + this.lifetime.toString(16)).slice(-8); var coeffHex = ('00' + this.coeff.toString(16)).slice(-2); var baseHex = ('00' + this.base.toString(16)).slice(-2); var bucketHex = this.buckets.map(function (b) { return b.toString(16); }).reduce(function (b1, b2) { return b1 + b2; }, ''); var discountHex = this.nonalphaDiscount.toString(16) + this.noVowelDiscount.toString(16); var versionHex = ('0000' + this.version.toString(16)).slice(-4); var namespaceIDHex = new Buffer(this.namespaceID).toString('hex'); return lifeHex + coeffHex + baseHex + bucketHex + discountHex + versionHex + namespaceIDHex; } }]); return BlockstackNamespace; }(); function asAmountV2(amount) { // convert an AmountType v1 or v2 to an AmountTypeV2. // the "units" of a v1 amount type are always 'BTC' if (typeof amount === 'number') { return { units: 'BTC', amount: _bigi2.default.fromByteArrayUnsigned(String(amount)) }; } else { return { units: amount.units, amount: amount.amount }; } } function makeTXbuilder() { var txb = new _bitcoinjsLib2.default.TransactionBuilder(_config.config.network.layer1); txb.setVersion(1); return txb; } function opEncode(opcode) { // NOTE: must *always* a 3-character string var res = '' + _config.config.network.MAGIC_BYTES + opcode; if (res.length !== 3) { throw new Error('Runtime error: invalid MAGIC_BYTES'); } return res; } function makePreorderSkeleton(fullyQualifiedName, consensusHash, preorderAddress, burnAddress, burn) { var registerAddress = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null; // Returns a preorder tx skeleton. // with 3 outputs : 1. the Blockstack Preorder OP_RETURN data // 2. the Preorder's change address (5500 satoshi minimum) // 3. the BURN // // 0 2 3 23 39 47 66 // |-----|--|--------------------------------------|--------------|-----------|-------------| // magic op hash160(fqn,scriptPubkey,registerAddr) consensus hash token burn token type // (optional) (optional) // // output 0: name preorder code // output 1: preorder address // output 2: burn address // // Returns an unsigned serialized transaction. var burnAmount = asAmountV2(burn); var network = _config.config.network; var nameBuff = Buffer.from((0, _utils.decodeB40)(fullyQualifiedName), 'hex'); // base40 var scriptPublicKey = _bitcoinjsLib2.default.address.toOutputScript(preorderAddress, network.layer1); var dataBuffers = [nameBuff, scriptPublicKey]; if (!!registerAddress) { var registerBuff = Buffer.from(registerAddress, 'ascii'); dataBuffers.push(registerBuff); } var dataBuff = Buffer.concat(dataBuffers); var hashed = (0, _utils.hash160)(dataBuff); var opReturnBufferLen = burnAmount.units === 'BTC' ? 39 : 66; var opReturnBuffer = Buffer.alloc(opReturnBufferLen); opReturnBuffer.write(opEncode('?'), 0, 3, 'ascii'); hashed.copy(opReturnBuffer, 3); opReturnBuffer.write(consensusHash, 23, 16, 'hex'); if (burnAmount.units !== 'BTC') { var burnHex = burnAmount.amount.toHex(); if (burnHex.length > 16) { // exceeds 2**64; can't fit throw new Error('Cannot preorder \'' + fullyQualifiedName + '\': cannot fit price into 8 bytes'); } var paddedBurnHex = ('0000000000000000' + burnHex).slice(-16); opReturnBuffer.write(paddedBurnHex, 39, 8, 'hex'); opReturnBuffer.write(burnAmount.units, 47, burnAmount.units.length, 'ascii'); } var nullOutput = _bitcoinjsLib2.default.payments.embed({ data: [opReturnBuffer] }).output; var tx = makeTXbuilder(); tx.addOutput(nullOutput, 0); tx.addOutput(preorderAddress, _utils.DUST_MINIMUM); if (burnAmount.units === 'BTC') { var btcBurnAmount = parseInt(burnAmount.amount.toHex(), 16); tx.addOutput(burnAddress, btcBurnAmount); } else { tx.addOutput(burnAddress, _utils.DUST_MINIMUM); } return tx.buildIncomplete(); } function makeRegisterSkeleton(fullyQualifiedName, ownerAddress) { var valueHash = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; var burnTokenAmountHex = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; // Returns a register tx skeleton. // with 2 outputs : 1. The register OP_RETURN // 2. The owner address (can be different from REGISTER address on renewals) // You MUST make the first input a UTXO from the current OWNER *or* the // funder of the PREORDER // in the case of a renewal, this would need to be modified to include a change address // as output (3) before the burn output (4) /* Formats No zonefile hash, and pay with BTC: 0 2 3 39 |----|--|----------------------------------| magic op name.ns_id (up to 37 bytes) With zonefile hash, and pay with BTC: 0 2 3 39 59 |----|--|----------------------------------|-------------------| magic op name.ns_id (37 bytes, 0-padded) zone file hash output 0: name registration code output 1: owner address */ var payload = void 0; if (!!burnTokenAmountHex && !valueHash) { // empty value hash valueHash = '0000000000000000000000000000000000000000'; } if (!!valueHash) { if (valueHash.length !== 40) { throw new Error('Value hash length incorrect. Expecting 20-bytes, hex-encoded'); } if (!!burnTokenAmountHex) { if (burnTokenAmountHex.length !== 16) { throw new Error('Burn field length incorrect. Expecting 8-bytes, hex-encoded'); } } var payloadLen = burnTokenAmountHex ? 65 : 57; payload = Buffer.alloc(payloadLen, 0); payload.write(fullyQualifiedName, 0, 37, 'ascii'); payload.write(valueHash, 37, 20, 'hex'); if (!!burnTokenAmountHex) { payload.write(burnTokenAmountHex, 57, 8, 'hex'); } } else { payload = Buffer.from(fullyQualifiedName, 'ascii'); } var opReturnBuffer = Buffer.concat([Buffer.from(opEncode(':'), 'ascii'), payload]); var nullOutput = _bitcoinjsLib2.default.payments.embed({ data: [opReturnBuffer] }).output; var tx = makeTXbuilder(); tx.addOutput(nullOutput, 0); tx.addOutput(ownerAddress, _utils.DUST_MINIMUM); return tx.buildIncomplete(); } function makeRenewalSkeleton(fullyQualifiedName, nextOwnerAddress, lastOwnerAddress, burnAddress, burn) { var valueHash = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null; /* Formats No zonefile hash, and pay with BTC: 0 2 3 39 |----|--|----------------------------------| magic op name.ns_id (up to 37 bytes) With zonefile hash, and pay with BTC: 0 2 3 39 59 |----|--|----------------------------------|-------------------| magic op name.ns_id (37 bytes, 0-padded) zone file hash With renewal payment in a token: (for register, tokens burned is not included) (for renew, tokens burned is the number of tokens to burn) 0 2 3 39 59 67 |----|--|----------------------------------|-------------------|------------------------------| magic op name.ns_id (37 bytes, 0-padded) zone file hash tokens burned (big-endian) output 0: renewal code output 1: new owner address output 2: current owner address output 3: burn address */ var burnAmount = asAmountV2(burn); var network = _config.config.network; var burnTokenAmount = burnAmount.units === 'BTC' ? null : burnAmount.amount; var burnBTCAmount = burnAmount.units === 'BTC' ? parseInt(burnAmount.amount.toHex(), 16) : _utils.DUST_MINIMUM; var burnTokenHex = null; if (!!burnTokenAmount) { var burnHex = burnTokenAmount.toHex(); if (burnHex.length > 16) { // exceeds 2**64; can't fit throw new Error('Cannot renew \'' + fullyQualifiedName + '\': cannot fit price into 8 bytes'); } burnTokenHex = ('0000000000000000' + burnHex).slice(-16); } var registerTX = makeRegisterSkeleton(fullyQualifiedName, nextOwnerAddress, valueHash, burnTokenHex); var txB = _bitcoinjsLib2.default.TransactionBuilder.fromTransaction(registerTX, network.layer1); txB.addOutput(lastOwnerAddress, _utils.DUST_MINIMUM); txB.addOutput(burnAddress, burnBTCAmount); return txB.buildIncomplete(); } function makeTransferSkeleton(fullyQualifiedName, consensusHash, newOwner) { var keepZonefile = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; // Returns a transfer tx skeleton. // with 2 outputs : 1. the Blockstack Transfer OP_RETURN data // 2. the new owner with a DUST_MINIMUM value (5500 satoshi) // // You MUST make the first input a UTXO from the current OWNER // // Returns an unsigned serialized transaction. /* Format 0 2 3 4 20 36 |-----|--|----|-------------------|---------------| magic op keep hash128(name.ns_id) consensus hash data? output 0: transfer code output 1: new owner */ var opRet = Buffer.alloc(36); var keepChar = '~'; if (keepZonefile) { keepChar = '>'; } opRet.write(opEncode('>'), 0, 3, 'ascii'); opRet.write(keepChar, 3, 1, 'ascii'); var hashed = (0, _utils.hash128)(Buffer.from(fullyQualifiedName, 'ascii')); hashed.copy(opRet, 4); opRet.write(consensusHash, 20, 16, 'hex'); var opRetPayload = _bitcoinjsLib2.default.payments.embed({ data: [opRet] }).output; var tx = makeTXbuilder(); tx.addOutput(opRetPayload, 0); tx.addOutput(newOwner, _utils.DUST_MINIMUM); return tx.buildIncomplete(); } function makeUpdateSkeleton(fullyQualifiedName, consensusHash, valueHash) { // Returns an update tx skeleton. // with 1 output : 1. the Blockstack update OP_RETURN // // You MUST make the first input a UTXO from the current OWNER // // Returns an unsigned serialized transaction. // // output 0: the revoke code /* Format: 0 2 3 19 39 |-----|--|-----------------------------------|-----------------------| magic op hash128(name.ns_id,consensus hash) hash160(data) output 0: update code */ var opRet = Buffer.alloc(39); var nameBuff = Buffer.from(fullyQualifiedName, 'ascii'); var consensusBuff = Buffer.from(consensusHash, 'ascii'); var hashedName = (0, _utils.hash128)(Buffer.concat([nameBuff, consensusBuff])); opRet.write(opEncode('+'), 0, 3, 'ascii'); hashedName.copy(opRet, 3); opRet.write(valueHash, 19, 20, 'hex'); var opRetPayload = _bitcoinjsLib2.default.payments.embed({ data: [opRet] }).output; var tx = makeTXbuilder(); tx.addOutput(opRetPayload, 0); return tx.buildIncomplete(); } function makeRevokeSkeleton(fullyQualifiedName) { // Returns a revoke tx skeleton // with 1 output: 1. the Blockstack revoke OP_RETURN // // You MUST make the first input a UTXO from the current OWNER // // Returns an unsigned serialized transaction /* Format: 0 2 3 39 |----|--|-----------------------------| magic op name.ns_id (37 bytes) output 0: the revoke code */ var opRet = Buffer.alloc(3); var nameBuff = Buffer.from(fullyQualifiedName, 'ascii'); opRet.write(opEncode('~'), 0, 3, 'ascii'); var opReturnBuffer = Buffer.concat([opRet, nameBuff]); var nullOutput = _bitcoinjsLib2.default.payments.embed({ data: [opReturnBuffer] }).output; var tx = makeTXbuilder(); tx.addOutput(nullOutput, 0); return tx.buildIncomplete(); } function makeNamespacePreorderSkeleton(namespaceID, consensusHash, preorderAddress, registerAddress, burn) { // Returns a namespace preorder tx skeleton. // Returns an unsigned serialized transaction. /* Formats: Without STACKS: 0 2 3 23 39 |-----|---|--------------------------------------|----------------| magic op hash(ns_id,script_pubkey,reveal_addr) consensus hash with STACKs: 0 2 3 23 39 47 |-----|---|--------------------------------------|----------------|--------------------------| magic op hash(ns_id,script_pubkey,reveal_addr) consensus hash token fee (big-endian) output 0: namespace preorder code output 1: change address otuput 2: burn address */ var burnAmount = asAmountV2(burn); if (burnAmount.units !== 'BTC' && burnAmount.units !== 'STACKS') { throw new Error('Invalid burnUnits ' + burnAmount.units); } var network = _config.config.network; var burnAddress = network.getDefaultBurnAddress(); var namespaceIDBuff = Buffer.from((0, _utils.decodeB40)(namespaceID), 'hex'); // base40 var scriptPublicKey = _bitcoinjsLib2.default.address.toOutputScript(preorderAddress, network.layer1); var registerBuff = Buffer.from(registerAddress, 'ascii'); var dataBuffers = [namespaceIDBuff, scriptPublicKey, registerBuff]; var dataBuff = Buffer.concat(dataBuffers); var hashed = (0, _utils.hash160)(dataBuff); var btcBurnAmount = _utils.DUST_MINIMUM; var opReturnBufferLen = 39; if (burnAmount.units === 'STACKS') { opReturnBufferLen = 47; } else { btcBurnAmount = parseInt(burnAmount.amount.toHex(), 16); } var opReturnBuffer = Buffer.alloc(opReturnBufferLen); opReturnBuffer.write(opEncode('*'), 0, 3, 'ascii'); hashed.copy(opReturnBuffer, 3); opReturnBuffer.write(consensusHash, 23, 16, 'hex'); if (burnAmount.units === 'STACKS') { var burnHex = burnAmount.amount.toHex(); var paddedBurnHex = ('0000000000000000' + burnHex).slice(-16); opReturnBuffer.write(paddedBurnHex, 39, 8, 'hex'); } var nullOutput = _bitcoinjsLib2.default.payments.embed({ data: [opReturnBuffer] }).output; var tx = makeTXbuilder(); tx.addOutput(nullOutput, 0); tx.addOutput(preorderAddress, _utils.DUST_MINIMUM); tx.addOutput(burnAddress, btcBurnAmount); return tx.buildIncomplete(); } function makeNamespaceRevealSkeleton(namespace, revealAddress) { /* Format: 0 2 3 7 8 9 10 11 12 13 14 15 16 17 18 20 39 |-----|---|----|-----|-----|----|----|----|----|----|-----|-----|-----|--------|-------|-------| magic op life coeff. base 1-2 3-4 5-6 7-8 9-10 11-12 13-14 15-16 nonalpha version ns ID bucket exponents no-vowel discounts output 0: namespace reveal code output 1: reveal address */ var hexPayload = namespace.toHexPayload(); var opReturnBuffer = Buffer.alloc(3 + hexPayload.length / 2); opReturnBuffer.write(opEncode('&'), 0, 3, 'ascii'); opReturnBuffer.write(hexPayload, 3, hexPayload.length / 2, 'hex'); var nullOutput = _bitcoinjsLib2.default.payments.embed({ data: [opReturnBuffer] }).output; var tx = makeTXbuilder(); tx.addOutput(nullOutput, 0); tx.addOutput(revealAddress, _utils.DUST_MINIMUM); return tx.buildIncomplete(); } function makeNamespaceReadySkeleton(namespaceID) { /* Format: 0 2 3 4 23 |-----|--|--|------------| magic op . ns_id output 0: namespace ready code */ var opReturnBuffer = Buffer.alloc(3 + namespaceID.length + 1); opReturnBuffer.write(opEncode('!'), 0, 3, 'ascii'); opReturnBuffer.write('.' + namespaceID, 3, namespaceID.length + 1, 'ascii'); var nullOutput = _bitcoinjsLib2.default.payments.embed({ data: [opReturnBuffer] }).output; var tx = makeTXbuilder(); tx.addOutput(nullOutput, 0); return tx.buildIncomplete(); } function makeNameImportSkeleton(name, recipientAddr, zonefileHash) { /* Format: 0 2 3 39 |----|--|-----------------------------| magic op name.ns_id (37 bytes) Output 0: the OP_RETURN Output 1: the recipient Output 2: the zonefile hash */ if (zonefileHash.length !== 40) { throw new Error('Invalid zonefile hash: must be 20 bytes hex-encoded'); } var network = _config.config.network; var opReturnBuffer = Buffer.alloc(3 + name.length); opReturnBuffer.write(opEncode(';'), 0, 3, 'ascii'); opReturnBuffer.write(name, 3, name.length, 'ascii'); var nullOutput = _bitcoinjsLib2.default.payments.embed({ data: [opReturnBuffer] }).output; var tx = makeTXbuilder(); var zonefileHashB58 = _bitcoinjsLib2.default.address.toBase58Check(new Buffer(zonefileHash, 'hex'), network.layer1.pubKeyHash); tx.addOutput(nullOutput, 0); tx.addOutput(recipientAddr, _utils.DUST_MINIMUM); tx.addOutput(zonefileHashB58, _utils.DUST_MINIMUM); return tx.buildIncomplete(); } function makeAnnounceSkeleton(messageHash) { /* Format: 0 2 3 23 |----|--|-----------------------------| magic op message hash (160-bit) output 0: the OP_RETURN */ if (messageHash.length !== 40) { throw new Error('Invalid message hash: must be 20 bytes hex-encoded'); } var opReturnBuffer = Buffer.alloc(3 + messageHash.length / 2); opReturnBuffer.write(opEncode('#'), 0, 3, 'ascii'); opReturnBuffer.write(messageHash, 3, messageHash.length / 2, 'hex'); var nullOutput = _bitcoinjsLib2.default.payments.embed({ data: [opReturnBuffer] }).output; var tx = makeTXbuilder(); tx.addOutput(nullOutput, 0); return tx.buildIncomplete(); } function makeTokenTransferSkeleton(recipientAddress, consensusHash, tokenType, tokenAmount, scratchArea) { /* Format: 0 2 3 19 38 46 80 |-----|--|--------------|----------|-----------|-------------------------| magic op consensus_hash token_type amount (BE) scratch area (ns_id) output 0: token transfer code output 1: recipient address */ if (scratchArea.length > 34) { throw new Error('Invalid scratch area: must be no more than 34 bytes'); } var opReturnBuffer = Buffer.alloc(46 + scratchArea.length); var tokenTypeHex = new Buffer(tokenType).toString('hex'); var tokenTypeHexPadded = ('00000000000000000000000000000000000000' + tokenTypeHex).slice(-38); var tokenValueHex = tokenAmount.toHex(); if (tokenValueHex.length > 16) { // exceeds 2**64; can't fit throw new Error('Cannot send tokens: cannot fit ' + tokenAmount.toString() + ' into 8 bytes'); } var tokenValueHexPadded = ('0000000000000000' + tokenValueHex).slice(-16); opReturnBuffer.write(opEncode('$'), 0, 3, 'ascii'); opReturnBuffer.write(consensusHash, 3, consensusHash.length / 2, 'hex'); opReturnBuffer.write(tokenTypeHexPadded, 19, tokenTypeHexPadded.length / 2, 'hex'); opReturnBuffer.write(tokenValueHexPadded, 38, tokenValueHexPadded.length / 2, 'hex'); opReturnBuffer.write(scratchArea, 46, scratchArea.length, 'ascii'); var nullOutput = _bitcoinjsLib2.default.payments.embed({ data: [opReturnBuffer] }).output; var tx = makeTXbuilder(); tx.addOutput(nullOutput, 0); tx.addOutput(recipientAddress, _utils.DUST_MINIMUM); return tx.buildIncomplete(); }