import * as bitcoin from '@oipwg/bitcoinjs-lib' import bitcoinMessage from '@oipwg/bitcoinjs-message' import { toBase58, isValidPublicAddress, isValidWIF } from './util' const ECPair = bitcoin.ECPair /** * [@oipwg/bitcoinjs-lib ECPair](https://github.com/bitcoinjs/@oipwg/bitcoinjs-lib/blob/master/src/ecpair.js#L16) * @typedef {Object} ECPair */ /** * Contains information about an Unspent Transaction Output * @typedef {Object} utxo * @property {string} address - Base58 Public Address * @property {string} txid - The Transaction ID * @property {number} vout - The Index of this specific Output in its parent Transaction * @property {string} scriptPubKey - The Script Public Key Hash * @property {number} amount - Amount (in whole Coin) of this Output * @property {number} satoshis - Amount in Satoshis of this Output * @property {number} height - The Blockheight the Parent Transaction was confirmed in * @property {number} confirmations - The total number of Confirmations the Parent Transaction has received * @example * { * address: 'F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp', * txid: '7687e361f00998f96b29938bf5b7d9003a15ec182c13b6ddbd5adc0f993cbf9c', * vout: 1, * scriptPubKey: '76a9141bfcff1731caf3a16225d3e78735ddc229e4fc6c88ac', * amount: 0.001, * satoshis: 100000, * height: 2784696, * confirmations: 6828 * } */ /** * Contains information about an Unspent Transaction Output * @typedef {Object} AddressState * @property {string} addrStr - Base58 Public Address * @property {number} balanceSat - Balance of the Address in Satoshis * @property {number} totalReceivedSat - Total Received to the Address in Satoshis * @property {number} unconfirmedBalanceSat - Unconfirmed Balance of the Address in Satoshis * @property {Array.<string>} transactions - Array of `txids` that have been confirmed on the Network * @property {Array.<string>} spentTransactions - Array of `txids` that have been spent, but not yet confirmed on the Network * @property {number} lastUpdated - Timestamp of when the Address was last updated/synced with the Explorer * @example * { * addrStr: 'F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp', * balanceSat: 0, * totalReceivedSat: 0, * unconfirmedBalanceSat: 0, * transactions: [], * spentTransactions: [], * lastUpdated: 0 * } */ /** * Manages information about a specific Address */ class Address { /** * Create a new Address based on either a bip32 node, WIF Private Key, or Public Address * ##### Examples * Create Address from bip32 * ``` * import * as bip32 from 'bip32'; * import { Address, Networks } from '@oipwg/hdmw'; * * let node = bip32.fromBase58("Fprv52CvMcVNkt3jU7MjybjTNie1Bqm7T66KBueSVFW74hXH43sXMAUdmk73TENACSHhHbwm7ZnHiaW3DxtkwhsbtpNjsh4EpnFVjZVJS7oxNqw", Networks.flo.network) * let address = new Address(node, Networks.flo); * ``` * Create Address from WIF * ``` * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("RAtKUeXYMEHEFkhbJuXGMEQZsqgHosnP2BLVaLWMRswWrcCNbZk5", Networks.flo); * ``` * Create Address from Base58 Public Address * ``` * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp", Networks.flo); * ``` * @param {bip32|string} address - The Public Address, Private Key (WIF), or bip32 Node that the Address is for. * @param {CoinInfo} coin - CoinInfo for the specific Address * @param {boolean|AddressState} [discover=false] - Either a `boolean` value for if the Address should auto-discover, or an AddressState object to load the Internal state from. * @return {Address} */ constructor (address, coin, discover) { if (address.network !== undefined) { this.fromBIP32 = true if (address.address) { this.address = address.address } else if (address.index !== undefined && address.depth !== undefined) { this.address = address } // Make sure that the networks match and throw an error if they don't if (address.network.pubKeyHash !== coin.network.pubKeyHash) { throw new Error('Address Network and Coin Network DO NOT MATCH!!!!!') } } else { if (isValidPublicAddress(address, coin.network)) { this.fromBIP32 = false this.pubAddress = address } else if (isValidWIF(address, coin.network)) { this.fromBIP32 = true this.address = ECPair.fromWIF(address, coin.network) } } this.coin = coin || { satPerCoin: 1e8 } // Setup internal variables this.transactions = [] this.balanceSat = 0 this.totalReceivedSat = 0 this.totalSentSat = 0 this.unconfirmedBalanceSat = 0 this.lastUpdated = 0 this.spentTransactions = [] if (discover === true) { // Update the state from the explorer this.updateState() } else if (discover) { // Load from serialized JSON this.deserialize(discover) } } /** * Get the Base58 sharable Public Address * @example * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("RAtKUeXYMEHEFkhbJuXGMEQZsqgHosnP2BLVaLWMRswWrcCNbZk5", Networks.flo); * let pubAddr = address.getPublicAddress(); * // pubAddr = F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp * @return {string} */ getPublicAddress () { let publicKey if (this.fromBIP32 && this.address) { publicKey = this.address.publicKey if (!publicKey && this.address.getPublicKeyBuffer) { publicKey = this.address.getPublicKeyBuffer() } } return this.fromBIP32 ? toBase58(publicKey, this.coin.network.pubKeyHash) : this.pubAddress } /** * Get the Base58 sharable Private Address (WIF) * @example * import * as bip32 from 'bip32'; * import { Address, Networks } from '@oipwg/hdmw'; * * let node = bip32.fromBase58("Fprv52CvMcVNkt3jU7MjybjTNie1Bqm7T66KBueSVFW74hXH43sXMAUdmk73TENACSHhHbwm7ZnHiaW3DxtkwhsbtpNjsh4EpnFVjZVJS7oxNqw", Networks.flo.network) * let address = new Address(node, Networks.flo); * let wif = address.getPrivateAddress(); * // wif = RAtKUeXYMEHEFkhbJuXGMEQZsqgHosnP2BLVaLWMRswWrcCNbZk5 * @return {string} */ getPrivateAddress () { return this.address ? this.address.toWIF() : undefined } /** * Get the internal ECPair. This is used when you need to Sign Transactions, or to access the raw public/private Buffers. * Please note that if you create the Address from a Public Key, you will not get back an ECPair, since we need access * to the Private Key in order to create/access the ECPair. When Address is created using a bip32 node or a Private Key (WIF) * the ECPair will exist. * @example * import * as bip32 from 'bip32'; * import { Address, Networks } from '@oipwg/hdmw'; * * let node = bip32.fromBase58("Fprv52CvMcVNkt3jU7MjybjTNie1Bqm7T66KBueSVFW74hXH43sXMAUdmk73TENACSHhHbwm7ZnHiaW3DxtkwhsbtpNjsh4EpnFVjZVJS7oxNqw", Networks.flo.network) * let address = new Address(node, Networks.flo); * let ecpair = address.getECPair(); * @return {ECPair} */ getECPair () { return this.address } /** * Get the signature of a specific message that can be verified by others * @param {String} message - The message you wish to get the signature for * @return {String} Returns the base64 string of the created Signature */ signMessage (message) { if (!message || typeof message !== 'string') { throw new Error('Message must be defined and a String!') } const privatekeyEcpair = this.getECPair() if (!privatekeyEcpair) { throw new Error('No Private Key available! Unable to sign message!') } const privateKeyBuffer = privatekeyEcpair.privateKey const compressed = privatekeyEcpair.compressed !== undefined ? privatekeyEcpair.compressed : true const messagePrefix = this.coin.network.messagePrefix let signatureBuffer try { signatureBuffer = bitcoinMessage.sign(message, privateKeyBuffer, compressed, messagePrefix) } catch (e) { throw new Error('Unable to create signature! \n' + e) } return signatureBuffer.toString('base64') } /** * Verify the signature of a given message * @param {String} message - The message you want to verify * @param {String} signature - The signature of the message * @return {Boolean} Returns either `true` or `false` depending on if the signature and message match */ verifySignature (message, signature) { let valid try { valid = bitcoinMessage.verify(message, this.getPublicAddress(), signature, this.coin.network.messagePrefix) } catch (e) { throw new Error('Unable to verify signature! \n' + e) } return valid } /** * Get the latest State for this address from the Blockchain Explorer * @example * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp", Networks.flo); * address.updateState().then((addr) => { * console.log(addr.getTotalReceived()) * }) * @return {Promise<Address>} Returns a Promise that will resolve to the Address */ async updateState () { let state try { state = await this.coin.explorer.getAddress(this.getPublicAddress()) } catch (e) { throw new Error('Error Updating Address State for: ' + this.getPublicAddress() + '\n' + e) } return this.deserialize(state) } /** * Hydrate an Address from the serialized JSON, or update the state * @param {AddressState} state * @example * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp", Networks.flo, false); * * address.deserialize({ * addrStr: 'F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp', * balanceSat: 123, * totalReceivedSat: 234, * unconfirmedBalanceSat: 345, * transactions: ['abcde'], * spentTransactions: ['bcdef'], * lastUpdated: 456 * }) * * let balance = address.getBalance() * // balance = 0.00000123 * @return {Address} */ deserialize (state) { if (!state) { return } // If the state doesn't match for this address, ignore it. if (state.addrStr && state.addrStr !== this.getPublicAddress()) { return } if (!isNaN(state.balanceSat)) { this.balanceSat = state.balanceSat } if (!isNaN(state.totalReceivedSat)) { this.totalReceivedSat = state.totalReceivedSat } if (!isNaN(state.totalSentSat)) { this.totalSentSat = state.totalSentSat } if (!isNaN(state.unconfirmedBalanceSat)) { this.unconfirmedBalanceSat = state.unconfirmedBalanceSat } if (Array.isArray(state.transactions)) { this.transactions = state.transactions } if (Array.isArray(state.spentTransactions)) { for (const tx of state.spentTransactions) { this.spentTransactions.push(tx) } } if (!isNaN(state.lastUpdated)) { this.lastUpdated = state.lastUpdated } else { this.lastUpdated = Date.now() } return this } /** * Get a serialized version of the Address (dried out JSON) * @example * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp", Networks.flo, false); * * let addressState = address.serialize() * // addressState = { * // addrStr: 'F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp', * // balanceSat: 0, * // totalReceivedSat: 0, * // unconfirmedBalanceSat: 0, * // transactions: [], * // spentTransactions: [], * // lastUpdated: 0 * // } * @return {AddressState} */ serialize () { return { addrStr: this.getPublicAddress(), wif: this.getPrivateAddress(), balanceSat: this.balanceSat, totalReceivedSat: this.totalReceivedSat, unconfirmedBalanceSat: this.unconfirmedBalanceSat, transactions: this.transactions, spentTransactions: this.spentTransactions, lastUpdated: this.lastUpdated } } /** * Get the Balance (in whole coins) for the Address * @example * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp", Networks.flo, false); * let balance = address.getBalance(); * // balance = 0 * @return {number} */ getBalance () { return this.balanceSat / this.coin.satPerCoin } /** * Get the Total Received balance (in whole coins) for the Address * @example * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp", Networks.flo, false); * let totReceived = address.getTotalReceived(); * // totReceived = 0 * @return {number} */ getTotalReceived () { return this.totalReceivedSat / this.coin.satPerCoin } /** * Get the Total Sent balance (in whole coins) for the Address * @example * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp", Networks.flo, false); * let totSent = address.getTotalSent(); * // totSent = 0 * @return {number} */ getTotalSent () { return this.totalSentSat / this.coin.satPerCoin } /** * Get the Unconfirmed Balance (in whole coins) for the Address * @example * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp", Networks.flo, false); * let uBal = address.getUnconfirmedBalance(); * // uBal = 0 * @return {number} */ getUnconfirmedBalance () { return this.unconfirmedBalanceSat / this.coin.satPerCoin } /** * Get the unspent transaction outputs for the Address * @example * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp", Networks.flo, false); * address.getUnspent().then((utxos) => { * console.log(utxos); * }) * @return {Promise<Array.<utxo>>} Returns a Promise that resolves to an Array of utxos. */ getUnspent () { return this.coin.explorer.getAddressUtxo(this.getPublicAddress()).then((utxos) => { return this.removeSpent(utxos) }) } /** * Remove the already spent outputs from the array we are given. * @param {Array.<utxo>} unspentTransactions - An Array containing utxos to sort through * @example * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp", Networks.flo, false); * address.getUnspent().then((utxos) => { * let unspentUtxos = address.removeSpent(utxos) * console.log(unspentUtxos) * }) * @return {Array.<utxo>} */ removeSpent (unspentTransactions) { // If we are not defined, or we are not an array, just return if (!unspentTransactions || !Array.isArray(unspentTransactions)) { return } const unspent = [] for (const tx of unspentTransactions) { let spent = false for (const txid of this.spentTransactions) { if (txid === tx.txid) { spent = true } } if (!spent) { unspent.push(tx) } } // @ToDo: Check if some spentTransactions txids are missing from the unspentTransactions array // If the txid is missing from the unspentTransactions array, then remove it from the spentTransactions array // This clears out any spentTransactions that have been confirmed as no longer unspent. return unspent } /** * Add a TXID to the local Spent Transactions of the Address to prevent a specific output from being doublespent. * @example * import { Address, Networks } from '@oipwg/hdmw'; * * let address = new Address("F8P6nUvDfcHikqdUnoQaGPBVxoMcUSpGDp", Networks.flo, false); * address.addSpentTransaction("7687e361f00998f96b29938bf5b7d9003a15ec182c13b6ddbd5adc0f993cbf9c") * @param {string} txid - The TXID of the spent output that we should remove */ addSpentTransaction (txid) { this.spentTransactions.push(txid) } } module.exports = Address