UNPKG

bmultisig

Version:

Bcoin wallet plugin for multi signature transaction proposals

1,281 lines (1,043 loc) 26.4 kB
/*! * client.js - Client for Multisig plugin * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License). * Copyright (c) 2019, Nodari Chkuaselidze (MIT License). * https://github.com/bcoin-org/bmultisig */ 'use strict'; const assert = require('bsert'); const EventEmitter = require('events'); const {WalletClient} = require('bcoin/lib/client'); /** * Hex encoded buffer. * @typedef {String} HexString */ /** * Multisig wallet client * @extends {bcoin#WalletClient} */ class MultisigClient extends WalletClient { /** * Create multisig client. * @param {Object} options - Wallet Client options */ constructor(options) { super(options); } /** * Start listening to multisig wallet events * @private */ init() { this.bind('join', (id, cosigner) => { this.dispatch(id, 'join', cosigner); }); this.bind('proposal created', (id, details) => { this.dispatch(id, 'proposal created', details); }); this.bind('proposal rejected', (id, details) => { this.dispatch(id, 'proposal rejected', details); }); this.bind('proposal approved', (id, details) => { this.dispatch(id, 'proposal approved', details); }); } /** * Open the client. * @returns {Promise} */ async open() { await super.open(); this.init(); } /** * Join a wallet. * @param {String} id - wallet id * @param {Buffer} token - cosigner token * @return {Promise} */ join(id, token) { return this.call('ms-join', id, token); } /** * Create a multisig wallet object * @param {String} id * @param {String|Buffer} token * @returns {MultisigWallet} */ wallet(id, token) { return new MultisigWallet(this, id, token); } /** * Get wallets (Admin only). * @returns {Promise<String[]>} list of wallets */ async getWallets() { const wallets = await this.get('/multisig'); // returns null when not configured properly if (!wallets) return wallets; return wallets.wallets; } /** * Create multisig wallet * @param {String} id * @param {Object} options * @returns {Promise<MultisigWallet>} walletInfo */ createWallet(id, options) { return this.put(`/multisig/${id}`, options); } /** * Remove multisig wallet (Admin only) * @param {Number|String} id * @returns {Promise<Boolean>} */ async removeWallet(id) { const removed = await this.del(`/multisig/${id}`); if (!removed) return false; return removed.success; } /** * Join wallet * @param {String} id * @param {Object} cosignerOptions * @returns {Promise<MultisigWallet>} */ joinWallet(id, cosignerOptions) { return this.post(`/multisig/${id}/join`, cosignerOptions); } /** * Get wallet transaction history. * @param {String} id * @returns {Promise} */ getHistory(id, account = 'default') { assert(account === 'default', 'Only default account available for multisig wallets.'); return this.get(`/multisig/${id}/tx/history`); } /** * Get wallet coins * @param {String} id * @returns {Promise<Coin[]>} */ getCoins(id, account = 'default') { assert(account === 'default', 'Only default account available for multisig wallets.'); return this.get(`/multisig/${id}/coin`); } /** * Get all unconfirmed transactions. * @param {String} id * @returns {Promise} */ getPending(id, account = 'default') { assert(account === 'default', 'Only default account available for multisig wallets.'); return this.get(`/multisig/${id}/tx/unconfirmed`); } /** * Get wallet balance * @param {String} id * @returns {Promise<bcoin#Balance>} */ getBalance(id, account = 'default') { assert(account === 'default', 'Only default account available for multisig wallets.'); return this.get(`/multisig/${id}/balance`); } /** * Get last N wallet transactions. * @param {String} id * @param {String} account * @param {Number} limit - Max number of transactions. * @returns {Promise} */ getLast(id, account = 'default', limit) { assert(account === 'default', 'Only default account available for multisig wallets.'); return this.get(`/multisig/${id}/tx/last`, { limit }); } /** * Get wallet transactions by timestamp range. * @param {String} id * @param {Object} options * @param {Number} options.start - Start time. * @param {Number} options.end - End time. * @param {Number?} options.limit - Max number of records. * @param {Boolean?} options.reverse - Reverse order. * @returns {Promise} */ getRange(id, account = 'default', options) { assert(account === 'default', 'Only default account available for multisig wallets.'); return this.get(`/multisig/${id}/tx/range`, { start: options.start, end: options.end, limit: options.limit, reverse: options.reverse }); } /** * Get transaction (only possible if the transaction * is available in the wallet history). * @param {String} id * @param {Hash} hash * @returns {Promise} */ getTX(id, hash) { return this.get(`/multisig/${id}/tx/${hash}`); } /** * Get wallet blocks. * @param {String} id * @param {Number} height * @returns {Promise} */ getBlocks(id) { return this.get(`/multisig/${id}/block`); } /** * Get wallet block. * @param {String} id * @param {Number} height * @returns {Promise} */ getBlock(id, height) { return this.get(`/multisig/${id}/block/${height}`); } /** * Get unspent coin (only possible if the transaction * is available in the wallet history). * @param {String} id * @param {Hash} hash * @param {Number} index * @returns {Promise} */ getCoin(id, hash, index) { return this.get(`/multisig/${id}/coin/${hash}/${index}`); } /** * @param {String} id * @param {Number} age - Age delta. * @returns {Promise} */ zap(id, account = 'default', age) { assert(account === 'default', 'Only default account available for multisig wallets.'); return this.post(`/multisig/${id}/zap`, { age }); } /** * Create wallet transaction * @param {String} id * @param {Object} options - transaction options * @returns {Promise<TX>} */ createTX(id, options) { return this.post(`/multisig/${id}/create`, options); } /** * Create a transaction, fill, sign, and broadcast. * @param {Object} options * @param {String} options.address * @param {Amount} options.value * @returns {Promise} */ send(id, options) { throw new Error('Cant use method "send" on multisig wallet.'); } /** * Sign a transaction. * @param {Object} options * @returns {Promise} */ sign(id, options) { throw new Error('Cant use method "sign" on multisig wallet.'); } /** * Get the raw wallet JSON. * @param {String} id * @returns {Promise<MultisigWallet|null>} */ getInfo(id) { return this.get(`/multisig/${id}`); } /** * Get wallet accounts. * @returns {Promise} - Returns Array. */ async getAccounts(id) { return ['default']; } /** * Get wallet master key. * @returns {Promise} */ getMaster(id) { throw new Error('Cant use method "getMaster" on multisig wallet.'); } /** * Get wallet account. * @param {String} account * @returns {Promise} */ async getAccount(id, account = 'default') { assert(account === 'default', 'Only default account available for multisig wallets.'); return this.get(`/multisig/${id}/account/${account}`); } /** * Create account. * @param {String} name * @param {Object} options * @returns {Promise} */ createAccount(id, name, options) { throw new Error('Cant use method "createAccount" on multisig wallet.'); } /** * Create address. * @param {String} id * @returns {Promise} */ createAddress(id, account = 'default') { assert(account === 'default', 'Only default account available for multisig wallets.'); return this.post(`/multisig/${id}/address`); } /** * Create change address. * @param {String} id * @returns {Promise} */ createChange(id, account = 'default') { assert(account === 'default', 'Only default account available for multisig wallets.'); return this.post(`/multisig/${id}/change`); } /** * Create nested address. * @param {String} id * @returns {Promise} */ createNested(id, account = 'default') { assert(account === 'default', 'Only default account available for multisig wallets.'); return this.post(`/multisig/${id}/nested`); } /** * Generate a new token. * @deprecated * @param {String} id * @returns {Promise} */ retoken(id) { return this.post(`/multisig/${id}/retoken`); } /** * Import private key. * @param {Number|String} account * @param {String} key * @returns {Promise} */ importPrivate(id, account, privateKey, passphrase) { throw new Error('Cant use method "importPrivate" on multisig wallet.'); } /** * Import public key. * @param {Number|String} account * @param {String} key * @returns {Promise} */ importPublic(id, account, publicKey) { throw new Error('Cant use method "importPublic" on multisig wallet.'); } /** * Import address. * @param {Number|String} account * @param {String} address * @returns {Promise} */ importAddress(id, account, address) { throw new Error('Cant use method "importAddress" on multisig wallet.'); } /** * Lock a coin. * @param {String} id * @param {String} hash * @param {Number} index * @returns {Promise} */ lockCoin(id, hash, index) { return this.put(`/multisig/${id}/locked/${hash}/${index}`); } /** * Unlock a coin. * @param {String} id * @param {String} hash * @param {Number} index * @param {Boolean} force - admin only * @returns {Promise} */ unlockCoin(id, hash, index, force) { return this.del(`/multisig/${id}/locked/${hash}/${index}`, { force }); } /** * Get locked coins. * @param {String} id - wallet id * @param {Boolean} filterProposal * @returns {Promise<Coin[]>} */ getLocked(id, filterProposal = false) { return this.get(`/multisig/${id}/locked/`, { proposalOnly: filterProposal }); } /** * Lock wallet. * @returns {Promise} */ lock(id) { throw new Error('Cant use method "lock" on multisig wallet.'); } /** * Unlock wallet. * @param {String} passphrase * @param {Number} timeout * @returns {Promise} */ unlock(id, passphrase, timeout) { throw new Error('Cant use method "unlock" on multisig wallet.'); } /** * Get wallet key. * @param {String} id * @param {String} address * @returns {Promise} */ getKey(id, address) { return this.get(`/multisig/${id}/key/${address}`); } /** * Get wallet key WIF dump. * @param {String} address * @param {String?} passphrase * @returns {Promise} */ getWIF(id, address, passphrase) { throw new Error('Cant use method "getWIF" on multisig wallet.'); } /** * Add a public account key to the wallet for multisig. * @param {String} account * @param {String} key - Account (bip44) key (base58). * @returns {Promise} */ addSharedKey(id, account, accountKey) { const text = 'Cant use method "addSharedKey" on multisig wallet. ' + 'Check "join" method instead.'; throw new Error(text); } /** * Remove a public account key to the wallet for multisig. * @param {String} account * @param {String} key - Account (bip44) key (base58). * @returns {Promise} */ removeSharedKey(id, account, accountKey) { const text = 'Cant use method "removeSharedKey" on multisig wallet. ' + 'Removing cosigner from multisig wallet is not allowed.'; throw new Error(text); } /** * Resend wallet transactions. * @param {String} id * @returns {Promise} */ resendWallet(id) { return this.post(`/multisig/${id}/resend`); } /* * Proposals */ /** * Get proposals * @param {String} id * @param {Boolean} [pending=true] * @returns {Promise<Proposal[]>} */ async getProposals(id, pending = true) { const proposalsObject = await this.get(`/multisig/${id}/proposal`, { pending }); if (!proposalsObject) return proposalsObject; return proposalsObject.proposals; } /** * Create proposal * @param {String} id * @param {Object} options - transaction options * @returns {Promise<Proposal>} */ createProposal(id, options) { return this.post(`/multisig/${id}/proposal`, options); } /** * Get proposal info * @param {String} id * @param {String} pid - proposal id * @param {Boolean} tx - get transaction * @returns {Promise<Proposal>} */ getProposalInfo(id, pid, tx) { return this.get(`/multisig/${id}/proposal/${pid}`, { tx }); } /** * Get proposal transaction * @param {String} id * @param {String} pid - proposal id * @param {Object} options * @param {Boolean} options.path - include input paths * @param {Boolean} options.tx - include input transactions * @param {Boolean} options.coin - include input coins * @returns {Promise<MTX>} */ getProposalMTX(id, pid, options) { return this.get(`/multisig/${id}/proposal/${pid}/tx`, options); } /** * Get proposal by UTXO. * @param {String} id * @param {Hash} hash * @param {Number} index * @returns {Promise<Proposal>} */ getProposalByCoin(id, hash, index) { return this.get(`/multisig/${id}/proposal/coin/${hash}/${index}`); } /** * Approve proposal * @param {String} id * @param {String} pid - proposal id * @param {Object} options * @param {HexString[]} options.signatures * @param {Boolean} options.broadcast * @returns {Promise<Proposal>} */ approveProposal(id, pid, options) { return this.post(`/multisig/${id}/proposal/${pid}/approve`, options); } /** * Reject proposal * @param {String} id * @param {String} pid - proposal id * @param {Object} options * @param {HexString} options.signature * @returns {Promise<Proposal>} */ rejectProposal(id, pid, options) { return this.post(`/multisig/${id}/proposal/${pid}/reject`, options); } /** * Send proposal tx * @param {String} id * @param {String} pid - proposal id * @returns {Promise<TX>} */ sendProposal(id, pid) { return this.post(`/multisig/${id}/proposal/${pid}/send`); } /** * Set a new token. * @param {String} id * @param {Object} options * @param {String} options.cosignerToken - hex string * @returns {Promise} */ setToken(id, options) { return this.put(`/multisig/${id}/token`, options); } /** * Export wallet * @param {String} id */ export(id) { return this.get(`/multisig/${id}/export`); } /** * Import wallet * @param {Options} options */ import(id, options) { return this.post('/multisig/import', { id: id, importOptions: options }); } } /** * Multisig wallet instance * @extends {EventEmitter} */ class MultisigWallet extends EventEmitter { /** * Create a multisig wallet client. * @param {MultisigClient} parent * @param {String} id * @param {String} token */ constructor(parent, id, token) { super(); this.parent = parent; this.client = parent.clone(); this.client.token = token; this.id = id; this.token = token; } /** * Open wallet. * @returns {Promise} */ async open() { await this.parent.join(this.id, this.token); this.parent.wallets.set(this.id, this); } /** * Close wallet. * @returns {Promise} */ async close() { await this.parent.leave(this.id); this.parent.wallets.delete(this.id); } /** * Remove multisig wallet (Admin only) * @returns {Promise<Boolean>} */ removeWallet() { return this.client.removeWallet(this.id); } /** * Join wallet * @param {Object} cosignerOptions * @returns {Promise<MultisigWallet|null>} */ joinWallet(cosignerOptions) { return this.client.joinWallet(this.id, cosignerOptions); } /** * Get wallet transaction history. * @returns {Promise} */ getHistory(account) { return this.client.getHistory(this.id, account); } /** * Get wallet coins * @returns {Promise<Coin[]>} */ getCoins(account) { return this.client.getCoins(this.id, account); } /** * Get all unconfirmed transactions. * @returns {Promise} */ getPending(account) { return this.client.getPending(this.id, account); } /** * Get wallet balance * @returns {Promise<bcoin#Balance>} */ getBalance(account) { return this.client.getBalance(this.id, account); } /** * Get last N wallet transactions. * @param {Number} limit - Max number of transactions. * @returns {Promise} */ getLast(account, limit) { return this.client.getLast(this.id, account, limit); } /** * Get wallet transactions by timestamp range. * @param {Object} options * @param {Number} options.start - Start time. * @param {Number} options.end - End time. * @param {Number?} options.limit - Max number of records. * @param {Boolean?} options.reverse - Reverse order. * @returns {Promise} */ getRange(account, options) { return this.client.getRange(this.id, account, options); } /** * Get transaction (only possible if the transaction * is available in the wallet history). * @param {Hash} hash * @returns {Promise} */ getTX(hash) { return this.client.getTX(this.id, hash); } /** * Get wallet blocks. * @param {Number} height * @returns {Promise} */ getBlocks() { return this.client.getBlocks(this.id); } /** * Get wallet block. * @param {Number} height * @returns {Promise} */ getBlock(height) { return this.client.getBlock(this.id, height); } /** * Get unspent coin (only possible if the transaction * is available in the wallet history). * @param {Hash} hash * @param {Number} index * @returns {Promise} */ getCoin(hash, index) { return this.client.getCoin(this.id, hash, index); } /** * @param {Number} now - Current time. * @param {Number} age - Age delta. * @returns {Promise} */ zap(account, age) { return this.client.zap(this.id, account, age); } /** * Create wallet transaction * @param {Object} options - transaction options * @returns {Promise<TX>} */ createTX(options) { return this.client.createTX(this.id, options); } /** * Create a transaction, fill, sign, and broadcast. * @param {Object} options * @param {String} options.address * @param {Amount} options.value * @returns {Promise} */ send(options) { return this.client.send(this.id, options); } /** * Sign a transaction. * @param {Object} options * @returns {Promise} */ sign(options) { return this.client.sign(this.id, options); } /** * Get the raw wallet JSON. * @param {Boolean} details * @returns {Promise} */ getInfo(details) { return this.client.getInfo(this.id, details); } /** * Get wallet accounts. * @returns {Promise} - Returns Array. */ getAccounts() { return this.client.getAccounts(this.id); } /** * Get wallet master key. * @returns {Promise} */ getMaster() { return this.client.getMaster(this.id); } /** * Get wallet account. * @param {String} account * @returns {Promise} */ getAccount(account) { return this.client.getAccount(this.id, account); } /** * Create account. * @param {String} name * @param {Object} options * @returns {Promise} */ createAccount(name, options) { return this.client.createAccount(this.id, name, options); } /** * Create address. * @returns {Promise} */ createAddress(account) { return this.client.createAddress(this.id, account); } /** * Create change address. * @returns {Promise} */ createChange(account) { return this.client.createChange(this.id, account); } /** * Create nested address. * @returns {Promise} */ createNested(account) { return this.client.createNested(this.id, account); } /** * Change or set master key`s passphrase. * @param {String|Buffer} passphrase * @param {(String|Buffer)?} old * @returns {Promise} */ setPassphrase(passphrase, old) { return this.client.setPassphrase(this.id, passphrase, old); } /** * Generate a new token. * @deprecated * @returns {Promise} */ retoken() { return this.client.retoken(this.id); } /** * Import private key. * @param {Number|String} account * @param {String} key * @returns {Promise} */ importPrivate(account, privateKey, passphrase) { return this.client.importPrivate(this.id, account, privateKey, passphrase); } /** * Import public key. * @param {Number|String} account * @param {String} key * @returns {Promise} */ importPublic(account, publicKey) { return this.client.importPublic(this.id, account, publicKey); } /** * Import address. * @param {Number|String} account * @param {String} address * @returns {Promise} */ importAddress(account, address) { return this.client.importAddress(this.id, account, address); } /** * Lock a coin. * @param {String} hash * @param {Number} index * @returns {Promise} */ lockCoin(hash, index) { return this.client.lockCoin(this.id, hash, index); } /** * Unlock a coin. * @param {String} hash * @param {Number} index * @param {Boolean} force * @returns {Promise} */ unlockCoin(hash, index, force) { return this.client.unlockCoin(this.id, hash, index, force); } /** * Get locked coins. * @param {Boolean} filterProposal * @returns {Promise} */ getLocked(filterProposal) { return this.client.getLocked(this.id, filterProposal); } /** * Lock wallet. * @returns {Promise} */ lock() { return this.client.lock(this.id); } /** * Unlock wallet. * @param {String} passphrase * @param {Number} timeout * @returns {Promise} */ unlock(passphrase, timeout) { return this.client.unlock(this.id, passphrase, timeout); } /** * Get wallet key. * @param {String} address * @returns {Promise} */ getKey(address) { return this.client.getKey(this.id, address); } /** * Get wallet key WIF dump. * @param {String} address * @param {String?} passphrase * @returns {Promise} */ getWIF(address, passphrase) { return this.client.getWIF(this.id, address, passphrase); } /** * Add a public account key to the wallet for multisig. * @param {String} account * @param {String} key - Account (bip44) key (base58). * @returns {Promise} */ addSharedKey(account, accountKey) { return this.client.addSharedKey(this.id, account, accountKey); } /** * Remove a public account key to the wallet for multisig. * @param {String} account * @param {String} key - Account (bip44) key (base58). * @returns {Promise} */ removeSharedKey(account, accountKey) { return this.client.removeSharedKey(this.id, account, accountKey); } /** * Resend wallet transactions. * @returns {Promise} */ resend() { return this.client.resendWallet(this.id); } /* * Proposals */ /** * Get proposals * @param {String} id * @param {Boolean} [pending=true] * @returns {Promise<Proposal[]>} */ getProposals(pending = true) { return this.client.getProposals(this.id, pending); } /** * Create proposal * @param {Object} options - transaction options * @returns {Promise<Proposal>} */ createProposal(options) { return this.client.createProposal(this.id, options); } /** * Get proposal info * @param {String} pid - proposal id * @param {Boolean} tx - get transaction * @returns {Promise<Proposal>} */ getProposalInfo(pid, tx) { return this.client.getProposalInfo(this.id, pid, tx); } /** * Get proposal transaction * @param {String} pid - proposal id * @param {Object} options * @param {Boolean} options.path - include input paths * @param {Boolean} options.scripts - include multisig scripts * @returns {Promise<MTX>} */ getProposalMTX(pid, options) { return this.client.getProposalMTX(this.id, pid, options); } /** * Get proposal by UTXO. * @param {Hash} hash * @param {Number} index * @returns {Promise<Proposal>} */ getProposalByCoin(hash, index) { return this.client.getProposalByCoin(this.id, hash, index); } /** * Approve proposal * @param {String} pid - proposal id * @param {Object} options * @param {HexString[]} options.signatures * @param {Boolean} options.broadcast * @returns {Promise<Proposal>} */ approveProposal(pid, options) { return this.client.approveProposal(this.id, pid, options); } /** * Reject proposal * @param {String} pid - proposal id * @param {Object} options * @param {HexString} options.signature * @returns {Promise<Proposal>} */ rejectProposal(pid, options) { return this.client.rejectProposal(this.id, pid, options); } /** * Force reject proposal (admin) * @param {String|Number} - proposal id * @returns {Promise<Proposal>} */ forceRejectProposal(pid) { return this.client.forceRejectProposal(this.id, pid); } /** * Send proposal tx * @param {String} pid - proposal id * @returns {Promise<TX>} */ sendProposal(pid) { return this.client.sendProposal(this.id, pid); } /** * Set a new token. * @param {Object} options * @returns {Promise<Cosigner>} */ setToken(options) { return this.client.setToken(this.id, options); } /** * Export wallet. */ export() { return this.client.export(this.id); } } /* * Expose */ module.exports = MultisigClient;