UNPKG

aladinnetwork-blockstack

Version:

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

1,175 lines (1,059 loc) 51.3 kB
import { TransactionBuilder, address as bjsAddress, TxOutput } from 'bitcoinjs-lib' import BN from 'bn.js' import { addUTXOsToFund, DUST_MINIMUM, estimateTXBytes, sumOutputValues, hash160, signInputs, getTransactionInsideBuilder } from './utils' import { makePreorderSkeleton, makeRegisterSkeleton, makeUpdateSkeleton, makeTransferSkeleton, makeRenewalSkeleton, makeRevokeSkeleton, makeNamespacePreorderSkeleton, makeNamespaceRevealSkeleton, makeNamespaceReadySkeleton, makeNameImportSkeleton, makeAnnounceSkeleton, makeTokenTransferSkeleton, AladinNamespace } from './skeletons' import { config } from '../config' import { InvalidAmountError, InvalidParameterError } from '../errors' import { TransactionSigner, PubkeyHashSigner } from './signers' import { UTXO } from '../network' const dummyConsensusHash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const dummyZonefileHash = 'ffffffffffffffffffffffffffffffffffffffff' /** * @ignore */ function addOwnerInput(utxos: UTXO[], ownerAddress: string, txB: TransactionBuilder, addChangeOut: boolean = true ) { // add an owner UTXO and a change out. if (utxos.length <= 0) { throw new Error('Owner has no UTXOs for UPDATE.') } utxos.sort((a, b) => a.value - b.value) const ownerUTXO = utxos[0] const ownerInput = txB.addInput(ownerUTXO.tx_hash, ownerUTXO.tx_output_n) if (addChangeOut) { txB.addOutput(ownerAddress, ownerUTXO.value) } return { index: ownerInput, value: ownerUTXO.value } } /** * @ignore */ function fundTransaction(txB: TransactionBuilder, paymentAddress: string, utxos: UTXO[], feeRate: number, inAmounts: number, changeIndex: number | null = null ) { // change index for the payer. if (changeIndex === null) { changeIndex = txB.addOutput(paymentAddress, DUST_MINIMUM) } // fund the transaction fee. const txFee = estimateTXBytes(txB, 0, 0) * feeRate const outAmounts = sumOutputValues(txB) const change = addUTXOsToFund(txB, utxos, txFee + outAmounts - inAmounts, feeRate) const txInner = getTransactionInsideBuilder(txB) const txOut = txInner.outs[changeIndex] as TxOutput txOut.value += change return txB } /** * @ignore */ function returnTransactionHex(txB: TransactionBuilder, buildIncomplete: boolean = false ) { if (buildIncomplete) { return txB.buildIncomplete().toHex() } else { return txB.build().toHex() } } /** * @ignore */ function getTransactionSigner(input: string | TransactionSigner): TransactionSigner { if (typeof input === 'string') { return PubkeyHashSigner.fromHexString(input) } else { return input } } /** * Estimates cost of a preorder transaction for a domain name. * @param {String} fullyQualifiedName - the name to preorder * @param {String} destinationAddress - the address to receive the name (this * must be passed as the 'registrationAddress' in the register transaction) * @param {String} paymentAddress - the address funding the preorder * @param {Number} paymentUtxos - the number of UTXOs we expect will be required * from the payment address. * @returns {Promise} - a promise which resolves to the satoshi cost to fund * the preorder. This includes a 5500 satoshi dust output for the preorder. * Even though this is a change output, the payer must supply enough funds * to generate this output, so we include it in the cost. * @private */ function estimatePreorder(fullyQualifiedName: string, destinationAddress: string, paymentAddress: string, paymentUtxos: number = 1 ): Promise<number> { const network = config.network const preorderPromise = network.getNamePrice(fullyQualifiedName) .then(namePrice => makePreorderSkeleton( fullyQualifiedName, dummyConsensusHash, paymentAddress, network.getDefaultBurnAddress(), namePrice, destinationAddress )) return Promise.all([network.getFeeRate(), preorderPromise]) .then(([feeRate, preorderTX]) => { const outputsValue = sumOutputValues(preorderTX) const txFee = feeRate * estimateTXBytes(preorderTX, paymentUtxos, 0) return txFee + outputsValue }) } /** * Estimates cost of a register transaction for a domain name. * @param {String} fullyQualifiedName - the name to register * @param {String} registerAddress - the address to receive the name * @param {String} paymentAddress - the address funding the register * @param {Boolean} includingZonefile - whether or not we will broadcast * a zonefile hash as part of the register * @param {Number} paymentUtxos - the number of UTXOs we expect will be required * from the payment address. * @returns {Promise} - a promise which resolves to the satoshi cost to fund * the register. * @private */ function estimateRegister(fullyQualifiedName: string, registerAddress: string, paymentAddress: string, includingZonefile: boolean = false, paymentUtxos: number = 1 ): Promise<number> { const network = config.network let valueHash if (includingZonefile) { valueHash = dummyZonefileHash } const registerTX = makeRegisterSkeleton( fullyQualifiedName, registerAddress, valueHash ) return network.getFeeRate() .then((feeRate) => { const outputsValue = sumOutputValues(registerTX) // 1 additional output for payer change const txFee = feeRate * estimateTXBytes(registerTX, paymentUtxos, 1) return txFee + outputsValue }) } /** * Estimates cost of an update transaction for a domain name. * @param {String} fullyQualifiedName - the name to update * @param {String} ownerAddress - the owner of the name * @param {String} paymentAddress - the address funding the update * @param {Number} paymentUtxos - the number of UTXOs we expect will be required * from the payment address. * @returns {Promise} - a promise which resolves to the satoshi cost to fund * the update. * @private */ function estimateUpdate(fullyQualifiedName: string, ownerAddress: string, paymentAddress: string, paymentUtxos: number = 1 ): Promise<number> { const network = config.network const updateTX = makeUpdateSkeleton( fullyQualifiedName, dummyConsensusHash, dummyZonefileHash ) return network.getFeeRate() .then((feeRate) => { const outputsValue = sumOutputValues(updateTX) // 1 additional input for the owner // 2 additional outputs for owner / payer change const txFee = feeRate * estimateTXBytes(updateTX, 1 + paymentUtxos, 2) return txFee + outputsValue }) } /** * Estimates cost of an transfer transaction for a domain name. * @param {String} fullyQualifiedName - the name to transfer * @param {String} destinationAddress - the next owner of the name * @param {String} ownerAddress - the current owner of the name * @param {String} paymentAddress - the address funding the transfer * @param {Number} paymentUtxos - the number of UTXOs we expect will be required * from the payment address. * @returns {Promise} - a promise which resolves to the satoshi cost to fund * the transfer. * @private */ function estimateTransfer(fullyQualifiedName: string, destinationAddress: string, ownerAddress: string, paymentAddress: string, paymentUtxos: number = 1 ): Promise<number> { const network = config.network const transferTX = makeTransferSkeleton(fullyQualifiedName, dummyConsensusHash, destinationAddress) return network.getFeeRate() .then((feeRate) => { const outputsValue = sumOutputValues(transferTX) // 1 additional input for the owner // 2 additional outputs for owner / payer change const txFee = feeRate * estimateTXBytes(transferTX, 1 + paymentUtxos, 2) return txFee + outputsValue }) } /** * Estimates cost of an transfer transaction for a domain name. * @param {String} fullyQualifiedName - the name to renew * @param {String} destinationAddress - the next owner of the name * @param {String} ownerAddress - the current owner of the name * @param {String} paymentAddress - the address funding the transfer * @param {Boolean} includingZonefile - whether or not we will broadcast a zonefile hash in the renewal operation * @param {Number} paymentUtxos - the number of UTXOs we expect will be required * from the payment address. * @returns {Promise} - a promise which resolves to the satoshi cost to fund * the transfer. * @private */ function estimateRenewal(fullyQualifiedName: string, destinationAddress: string, ownerAddress: string, paymentAddress: string, includingZonefile: boolean = false, paymentUtxos: number = 1 ): Promise<number> { const network = config.network let valueHash: string if (includingZonefile) { valueHash = dummyZonefileHash } const renewalPromise = network.getNamePrice(fullyQualifiedName) .then(namePrice => makeRenewalSkeleton( fullyQualifiedName, destinationAddress, ownerAddress, network.getDefaultBurnAddress(), namePrice, valueHash )) return Promise.all([network.getFeeRate(), renewalPromise]) .then(([feeRate, renewalTX]) => { const outputsValue = sumOutputValues(renewalTX) // 1 additional input for the owner // and renewal skeleton includes all outputs for owner change, but not for payer change. const txFee = feeRate * estimateTXBytes(renewalTX, 1 + paymentUtxos, 1) return txFee + outputsValue - 5500 // don't count the dust change for old owner. }) } /** * Estimates cost of a revoke transaction for a domain name. * @param {String} fullyQualifiedName - the name to revoke * @param {String} ownerAddress - the current owner of the name * @param {String} paymentAddress the address funding the revoke * @param {Number} paymentUtxos - the number of UTXOs we expect will be required * from the payment address. * @returns {Promise} - a promise which resolves to the satoshi cost to fund the * revoke. * @private */ function estimateRevoke(fullyQualifiedName: string, ownerAddress: string, paymentAddress: string, paymentUtxos: number = 1 ): Promise<number> { const network = config.network const revokeTX = makeRevokeSkeleton(fullyQualifiedName) return Promise.all([network.getFeeRate()]) .then(([feeRate]) => { const outputsValue = sumOutputValues(revokeTX) // 1 additional input for owner // 1 additional output for payer change const txFee = feeRate * estimateTXBytes(revokeTX, 1 + paymentUtxos, 2) return txFee + outputsValue }) } /** * Estimates cost of a namespace preorder transaction for a namespace * @param {String} namespaceID - the namespace to preorder * @param {String} revealAddress - the address to receive the namespace (this * must be passed as the 'revealAddress' in the namespace-reveal transaction) * @param {String} paymentAddress - the address funding the preorder * @param {Number} paymentUtxos - the number of UTXOs we expect will be required * from the payment address. * @returns {Promise} - a promise which resolves to the satoshi cost to fund * the preorder. This includes a 5500 satoshi dust output for the preorder. * Even though this is a change output, the payer must supply enough funds * to generate this output, so we include it in the cost. * @private */ function estimateNamespacePreorder(namespaceID: string, revealAddress: string, paymentAddress: string, paymentUtxos: number = 1 ): Promise<number> { const network = config.network const preorderPromise = network.getNamespacePrice(namespaceID) .then(namespacePrice => makeNamespacePreorderSkeleton( namespaceID, dummyConsensusHash, paymentAddress, revealAddress, namespacePrice )) return Promise.all([network.getFeeRate(), preorderPromise]) .then(([feeRate, preorderTX]) => { const outputsValue = sumOutputValues(preorderTX) const txFee = feeRate * estimateTXBytes(preorderTX, paymentUtxos, 0) return txFee + outputsValue }) } /** * Estimates cost of a namesapce reveal transaction for a namespace * @param {AladinNamespace} namespace - the namespace to reveal * @param {String} revealAddress - the address to receive the namespace * (this must have been passed as 'revealAddress' to a prior namespace * preorder) * @param {String} paymentAddress - the address that pays for this transaction * @param {Number} paymentUtxos - the number of UTXOs we expect will be required * from the payment address * @returns {Promise} - a promise which resolves to the satoshi cost to * fund the reveal. This includes a 5500 satoshi dust output for the * preorder. Even though this is a change output, the payer must have * enough funds to generate this output, so we include it in the cost. * @private */ function estimateNamespaceReveal(namespace: AladinNamespace, revealAddress: string, paymentAddress: string, paymentUtxos: number = 1 ): Promise<number> { const network = config.network const revealTX = makeNamespaceRevealSkeleton(namespace, revealAddress) return network.getFeeRate() .then((feeRate) => { const outputsValue = sumOutputValues(revealTX) // 1 additional output for payer change const txFee = feeRate * estimateTXBytes(revealTX, paymentUtxos, 1) return txFee + outputsValue }) } /** * Estimates the cost of a namespace-ready transaction for a namespace * @param {String} namespaceID - the namespace to ready * @param {Number} revealUtxos - the number of UTXOs we expect will * be required from the reveal address * @returns {Promise} - a promise which resolves to the satoshi cost to * fund this namespacey-ready transaction. * @private */ function estimateNamespaceReady(namespaceID: string, revealUtxos: number = 1 ): Promise<number> { const network = config.network const readyTX = makeNamespaceReadySkeleton(namespaceID) return network.getFeeRate() .then((feeRate) => { const outputsValue = sumOutputValues(readyTX) const txFee = feeRate * estimateTXBytes(readyTX, revealUtxos, 1) return txFee + outputsValue }) } /** * Estimates the cost of a name-import transaction * @param {String} name - the fully-qualified name * @param {String} recipientAddr - the recipient * @param {String} zonefileHash - the zone file hash * @param {Number} importUtxos - the number of UTXOs we expect will * be required from the importer address * @returns {Promise} - a promise which resolves to the satoshi cost * to fund this name-import transaction * @private */ function estimateNameImport(name: string, recipientAddr: string, zonefileHash: string, importUtxos: number = 1 ): Promise<number> { const network = config.network const importTX = makeNameImportSkeleton(name, recipientAddr, zonefileHash) return network.getFeeRate() .then((feeRate) => { const outputsValue = sumOutputValues(importTX) const txFee = feeRate * estimateTXBytes(importTX, importUtxos, 1) return txFee + outputsValue }) } /** * Estimates the cost of an announce transaction * @param {String} messageHash - the hash of the message * @param {Number} senderUtxos - the number of utxos we expect will * be required from the importer address * @returns {Promise} - a promise which resolves to the satoshi cost * to fund this announce transaction * @private */ function estimateAnnounce(messageHash: string, senderUtxos: number = 1 ): Promise<number> { const network = config.network const announceTX = makeAnnounceSkeleton(messageHash) return network.getFeeRate() .then((feeRate) => { const outputsValue = sumOutputValues(announceTX) const txFee = feeRate * estimateTXBytes(announceTX, senderUtxos, 1) return txFee + outputsValue }) } /** * Estimates the cost of a token-transfer transaction * @param {String} recipientAddress - the recipient of the tokens * @param {String} tokenType - the type of token to spend * @param {Object} tokenAmount - a 64-bit unsigned BigInteger encoding the number of tokens * to spend * @param {String} scratchArea - an arbitrary string to store with the transaction * @param {Number} senderUtxos - the number of utxos we expect will * be required from the importer address * @param {Number} additionalOutputs - the number of outputs we expect to add beyond * just the recipient output (default = 1, if the token owner is also the bitcoin funder) * @returns {Promise} - a promise which resolves to the satoshi cost to * fund this token-transfer transaction * @private */ function estimateTokenTransfer(recipientAddress: string, tokenType: string, tokenAmount: BN, scratchArea: string, senderUtxos: number = 1, additionalOutputs: number = 1 ) { const network = config.network const tokenTransferTX = makeTokenTransferSkeleton( recipientAddress, dummyConsensusHash, tokenType, tokenAmount, scratchArea) return network.getFeeRate() .then((feeRate) => { const outputsValue = sumOutputValues(tokenTransferTX) const txFee = feeRate * estimateTXBytes(tokenTransferTX, senderUtxos, additionalOutputs) return txFee + outputsValue }) } /** * Generates a preorder transaction for a domain name. * @param {String} fullyQualifiedName - the name to pre-order * @param {String} destinationAddress - the address to receive the name (this * must be passed as the 'registrationAddress' in the register transaction) * @param {String | TransactionSigner} paymentKeyIn - a hex string of * the private key used to fund the transaction or a transaction signer object * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * this function *does not* perform the requisite safety checks -- please see * the safety module for those. * @private */ function makePreorder(fullyQualifiedName: string, destinationAddress: string, paymentKeyIn: string | TransactionSigner, buildIncomplete: boolean = false ) { const network = config.network const namespace = fullyQualifiedName.split('.').pop() const paymentKey = getTransactionSigner(paymentKeyIn) return paymentKey.getAddress().then((preorderAddress) => { const preorderPromise = Promise.all([network.getConsensusHash(), network.getNamePrice(fullyQualifiedName), network.getNamespaceBurnAddress(namespace)]) .then(([consensusHash, namePrice, burnAddress]) => makePreorderSkeleton( fullyQualifiedName, consensusHash, preorderAddress, burnAddress, namePrice, destinationAddress )) return Promise.all([network.getUTXOs(preorderAddress), network.getFeeRate(), preorderPromise]) .then(([utxos, feeRate, preorderSkeleton]) => { const txB = TransactionBuilder.fromTransaction(preorderSkeleton, network.layer1) txB.setVersion(1) const changeIndex = 1 // preorder skeleton always creates a change output at index = 1 const signingTxB = fundTransaction(txB, preorderAddress, utxos, feeRate, 0, changeIndex) return signInputs(signingTxB, paymentKey) }) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) }) } /** * Generates an update transaction for a domain name. * @param {String} fullyQualifiedName - the name to update * @param {String | TransactionSigner} ownerKeyIn - a hex string of the * owner key, or a transaction signer object. This will provide one * UTXO input, and also recieve a dust output. * @param {String | TransactionSigner} paymentKeyIn - a hex string, or a * transaction signer object, of the private key used to fund the * transaction's txfees * @param {String} zonefile - the zonefile data to update (this will be hashed * to include in the transaction), the zonefile itself must be published * after the UPDATE propagates. * @param {String} valueHash - if given, this is the hash to store (instead of * zonefile). zonefile will be ignored if this is given. * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * this function *does not* perform the requisite safety checks -- please see * the safety module for those. * @private */ function makeUpdate(fullyQualifiedName: string, ownerKeyIn: string | TransactionSigner, paymentKeyIn: string | TransactionSigner, zonefile: string, valueHash: string = '', buildIncomplete: boolean = false ) { const network = config.network if (!valueHash && !zonefile) { return Promise.reject( new Error('Need zonefile or valueHash arguments') ) } if (valueHash.length === 0) { if (!zonefile) { return Promise.reject( new Error('Need zonefile or valueHash arguments') ) } valueHash = hash160(Buffer.from(zonefile)).toString('hex') } else if (valueHash.length !== 40) { return Promise.reject( new Error(`Invalid valueHash ${valueHash}`) ) } const paymentKey = getTransactionSigner(paymentKeyIn) const ownerKey = getTransactionSigner(ownerKeyIn) return Promise.all([ownerKey.getAddress(), paymentKey.getAddress()]) .then(([ownerAddress, paymentAddress]) => { const txPromise = network.getConsensusHash() .then(consensusHash => makeUpdateSkeleton(fullyQualifiedName, consensusHash, valueHash)) .then((updateTX) => { const txB = TransactionBuilder.fromTransaction(updateTX, network.layer1) txB.setVersion(1) return txB }) return Promise.all([txPromise, network.getUTXOs(paymentAddress), network.getUTXOs(ownerAddress), network.getFeeRate()]) .then(([txB, payerUtxos, ownerUtxos, feeRate]) => { const ownerInput = addOwnerInput(ownerUtxos, ownerAddress, txB) const signingTxB = fundTransaction(txB, paymentAddress, payerUtxos, feeRate, ownerInput.value) return signInputs(signingTxB, paymentKey, [{ index: ownerInput.index, signer: ownerKey }]) }) }) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) } /** * Generates a register transaction for a domain name. * @param {String} fullyQualifiedName - the name to register * @param {String} registerAddress - the address to receive the name (this * must have been passed as the 'destinationAddress' in the preorder transaction) * this address will receive a dust UTXO * @param {String | TransactionSigner} paymentKeyIn - a hex string of * the private key (or a TransactionSigner object) used to fund the * transaction (this *must* be the same as the payment address used * to fund the preorder) * @param {String} zonefile - the zonefile data to include (this will be hashed * to include in the transaction), the zonefile itself must be published * after the UPDATE propagates. * @param {String} valueHash - the hash of the zone file data to include. * It will be used instead of zonefile, if given * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * this function *does not* perform the requisite safety checks -- please see * the safety module for those. * @private */ function makeRegister(fullyQualifiedName: string, registerAddress: string, paymentKeyIn: string | TransactionSigner, zonefile: string = null, valueHash: string = null, buildIncomplete: boolean = false ) { const network = config.network if (!valueHash && !!zonefile) { valueHash = hash160(Buffer.from(zonefile)).toString('hex') } else if (!!valueHash && valueHash.length !== 40) { return Promise.reject( new Error(`Invalid zonefile hash ${valueHash}`) ) } const registerSkeleton = makeRegisterSkeleton( fullyQualifiedName, registerAddress, valueHash ) const txB = TransactionBuilder.fromTransaction(registerSkeleton, network.layer1) txB.setVersion(1) const paymentKey = getTransactionSigner(paymentKeyIn) return paymentKey.getAddress().then( paymentAddress => Promise.all([network.getUTXOs(paymentAddress), network.getFeeRate()]) .then(([utxos, feeRate]) => { const signingTxB = fundTransaction(txB, paymentAddress, utxos, feeRate, 0) return signInputs(signingTxB, paymentKey) }) ) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) } /** * Generates a transfer transaction for a domain name. * @param {String} fullyQualifiedName - the name to transfer * @param {String} destinationAddress - the address to receive the name. * this address will receive a dust UTXO * @param {String | TransactionSigner} ownerKeyIn - a hex string of * the current owner's private key (or a TransactionSigner object) * @param {String | TransactionSigner} paymentKeyIn - a hex string of * the private key used to fund the transaction (or a * TransactionSigner object) * @param {Boolean} keepZonefile - if true, then preserve the name's zone file * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * this function *does not* perform the requisite safety checks -- please see * the safety module for those. * @private */ function makeTransfer(fullyQualifiedName: string, destinationAddress: string, ownerKeyIn: string | TransactionSigner, paymentKeyIn: string | TransactionSigner, keepZonefile: boolean = false, buildIncomplete: boolean = false ) { const network = config.network const paymentKey = getTransactionSigner(paymentKeyIn) const ownerKey = getTransactionSigner(ownerKeyIn) return Promise.all([ownerKey.getAddress(), paymentKey.getAddress()]) .then(([ownerAddress, paymentAddress]) => { const txPromise = network.getConsensusHash() .then(consensusHash => makeTransferSkeleton( fullyQualifiedName, consensusHash, destinationAddress, keepZonefile )) .then((transferTX) => { const txB = TransactionBuilder .fromTransaction(transferTX, network.layer1) txB.setVersion(1) return txB }) return Promise.all([txPromise, network.getUTXOs(paymentAddress), network.getUTXOs(ownerAddress), network.getFeeRate()]) .then(([txB, payerUtxos, ownerUtxos, feeRate]) => { const ownerInput = addOwnerInput(ownerUtxos, ownerAddress, txB) const signingTxB = fundTransaction(txB, paymentAddress, payerUtxos, feeRate, ownerInput.value) return signInputs(signingTxB, paymentKey, [{ index: ownerInput.index, signer: ownerKey }]) }) }) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) } /** * Generates a revoke transaction for a domain name. * @param {String} fullyQualifiedName - the name to revoke * @param {String | TransactionSigner} ownerKeyIn - a hex string of * the current owner's private key (or a TransactionSigner object) * @param {String | TransactionSigner} paymentKeyIn - a hex string of * the private key used to fund the transaction (or a * TransactionSigner object) * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * this function *does not* perform the requisite safety checks -- please see * the safety module for those. * @private */ function makeRevoke(fullyQualifiedName: string, ownerKeyIn: string | TransactionSigner, paymentKeyIn: string | TransactionSigner, buildIncomplete: boolean = false ) { const network = config.network const paymentKey = getTransactionSigner(paymentKeyIn) const ownerKey = getTransactionSigner(ownerKeyIn) return Promise.all([ownerKey.getAddress(), paymentKey.getAddress()]) .then(([ownerAddress, paymentAddress]) => { const revokeTX = makeRevokeSkeleton(fullyQualifiedName) const txPromise = TransactionBuilder.fromTransaction(revokeTX, network.layer1) txPromise.setVersion(1) return Promise.all([txPromise, network.getUTXOs(paymentAddress), network.getUTXOs(ownerAddress), network.getFeeRate()]) .then(([txB, payerUtxos, ownerUtxos, feeRate]) => { const ownerInput = addOwnerInput(ownerUtxos, ownerAddress, txB) const signingTxB = fundTransaction(txB, paymentAddress, payerUtxos, feeRate, ownerInput.value) return signInputs(signingTxB, paymentKey, [{ index: ownerInput.index, signer: ownerKey }]) }) }) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) } /** * Generates a renewal transaction for a domain name. * @param {String} fullyQualifiedName - the name to transfer * @param {String} destinationAddress - the address to receive the name after renewal * this address will receive a dust UTXO * @param {String | TransactionSigner} ownerKeyIn - a hex string of * the current owner's private key (or a TransactionSigner object) * @param {String | TransactionSigner} paymentKeyIn - a hex string of * the private key used to fund the renewal (or a TransactionSigner * object) * @param {String} zonefile - the zonefile data to include, if given (this will be hashed * to include in the transaction), the zonefile itself must be published * after the RENEWAL propagates. * @param {String} valueHash - the raw zone file hash to include (this will be used * instead of zonefile, if given). * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * this function *does not* perform the requisite safety checks -- please see * the safety module for those. * @private */ function makeRenewal(fullyQualifiedName: string, destinationAddress: string, ownerKeyIn: string | TransactionSigner, paymentKeyIn: string | TransactionSigner, zonefile: string = null, valueHash: string = null, buildIncomplete: boolean = false ) { const network = config.network if (!valueHash && !!zonefile) { valueHash = hash160(Buffer.from(zonefile)).toString('hex') } const namespace = fullyQualifiedName.split('.').pop() const paymentKey = getTransactionSigner(paymentKeyIn) const ownerKey = getTransactionSigner(ownerKeyIn) return Promise.all([ownerKey.getAddress(), paymentKey.getAddress()]) .then(([ownerAddress, paymentAddress]) => { const txPromise = Promise.all([network.getNamePrice(fullyQualifiedName), network.getNamespaceBurnAddress(namespace)]) .then(([namePrice, burnAddress]) => makeRenewalSkeleton( fullyQualifiedName, destinationAddress, ownerAddress, burnAddress, namePrice, valueHash )) .then((tx) => { const txB = TransactionBuilder.fromTransaction(tx, network.layer1) txB.setVersion(1) return txB }) return Promise.all([txPromise, network.getUTXOs(paymentAddress), network.getUTXOs(ownerAddress), network.getFeeRate()]) .then(([txB, payerUtxos, ownerUtxos, feeRate]) => { const ownerInput = addOwnerInput(ownerUtxos, ownerAddress, txB, false) const txInner = getTransactionInsideBuilder(txB) const ownerOutput = txInner.outs[2] as TxOutput const ownerOutputAddr = bjsAddress.fromOutputScript( ownerOutput.script, network.layer1 ) if (ownerOutputAddr !== ownerAddress) { return Promise.reject( new Error(`Original owner ${ownerAddress} should have an output at ` + `index 2 in transaction was ${ownerOutputAddr}`) ) } ownerOutput.value = ownerInput.value const signingTxB = fundTransaction(txB, paymentAddress, payerUtxos, feeRate, ownerInput.value) return signInputs(signingTxB, paymentKey, [{ index: ownerInput.index, signer: ownerKey }]) }) }) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) } /** * Generates a namespace preorder transaction for a namespace * @param {String} namespaceID - the namespace to pre-order * @param {String} revealAddress - the address to receive the namespace (this * must be passed as the 'revealAddress' in the namespace-reveal transaction) * @param {String | TransactionSigner} paymentKeyIn - a hex string of * the private key used to fund the transaction (or a * TransactionSigner object) * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * this function *does not* perform the requisite safety checks -- please see * the safety module for those. * @private * * @ignore */ function makeNamespacePreorder(namespaceID: string, revealAddress: string, paymentKeyIn: string | TransactionSigner, buildIncomplete: boolean = false ) { const network = config.network const paymentKey = getTransactionSigner(paymentKeyIn) return paymentKey.getAddress().then((preorderAddress) => { const preorderPromise = Promise.all([network.getConsensusHash(), network.getNamespacePrice(namespaceID)]) .then(([consensusHash, namespacePrice]) => makeNamespacePreorderSkeleton( namespaceID, consensusHash, preorderAddress, revealAddress, namespacePrice)) return Promise.all([network.getUTXOs(preorderAddress), network.getFeeRate(), preorderPromise]) .then(([utxos, feeRate, preorderSkeleton]) => { const txB = TransactionBuilder.fromTransaction(preorderSkeleton, network.layer1) txB.setVersion(1) const changeIndex = 1 // preorder skeleton always creates a change output at index = 1 const signingTxB = fundTransaction(txB, preorderAddress, utxos, feeRate, 0, changeIndex) return signInputs(signingTxB, paymentKey) }) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) }) } /** * Generates a namespace reveal transaction for a namespace * @param {AladinNamespace} namespace - the namespace to reveal * @param {String} revealAddress - the address to receive the namespace (this * must be passed as the 'revealAddress' in the namespace-reveal transaction) * @param {String | TransactionSigner} paymentKeyIn - a hex string (or * a TransactionSigner object) of the private key used to fund the * transaction * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * this function *does not* perform the requisite safety checks -- please see * the safety module for those. * @private */ function makeNamespaceReveal(namespace: AladinNamespace, revealAddress: string, paymentKeyIn: string | TransactionSigner, buildIncomplete: boolean = false ) { const network = config.network if (!namespace.check()) { return Promise.reject(new Error('Invalid namespace')) } const namespaceRevealTX = makeNamespaceRevealSkeleton(namespace, revealAddress) const paymentKey = getTransactionSigner(paymentKeyIn) return paymentKey.getAddress().then( preorderAddress => Promise.all([network.getUTXOs(preorderAddress), network.getFeeRate()]) .then(([utxos, feeRate]) => { const txB = TransactionBuilder .fromTransaction(namespaceRevealTX, network.layer1) txB.setVersion(1) const signingTxB = fundTransaction(txB, preorderAddress, utxos, feeRate, 0) return signInputs(signingTxB, paymentKey) }) ) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) } /** * Generates a namespace ready transaction for a namespace * @param {String} namespaceID - the namespace to launch * @param {String | TransactionSigner} revealKeyIn - the private key * of the 'revealAddress' used to reveal the namespace * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * this function *does not* perform the requisite safety checks -- please see * the safety module for those. * @private */ function makeNamespaceReady(namespaceID: string, revealKeyIn: string | TransactionSigner, buildIncomplete: boolean = false ) { const network = config.network const namespaceReadyTX = makeNamespaceReadySkeleton(namespaceID) const revealKey = getTransactionSigner(revealKeyIn) return revealKey.getAddress().then( revealAddress => Promise.all([network.getUTXOs(revealAddress), network.getFeeRate()]) .then(([utxos, feeRate]) => { const txB = TransactionBuilder.fromTransaction(namespaceReadyTX, network.layer1) txB.setVersion(1) const signingTxB = fundTransaction(txB, revealAddress, utxos, feeRate, 0) return signInputs(signingTxB, revealKey) }) ) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) } /** * Generates a name import transaction for a namespace * @param {String} name - the name to import * @param {String} recipientAddr - the address to receive the name * @param {String} zonefileHash - the hash of the zonefile to give this name * @param {String | TransactionSigner} importerKeyIn - the private key * that pays for the import * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * this function does not perform the requisite safety checks -- please see * the safety module for those. * @private */ function makeNameImport(name: string, recipientAddr: string, zonefileHash: string, importerKeyIn: string | TransactionSigner, buildIncomplete: boolean = false ) { const network = config.network const nameImportTX = makeNameImportSkeleton(name, recipientAddr, zonefileHash) const importerKey = getTransactionSigner(importerKeyIn) return importerKey.getAddress().then( importerAddress => Promise.all([network.getUTXOs(importerAddress), network.getFeeRate()]) .then(([utxos, feeRate]) => { const txB = TransactionBuilder.fromTransaction(nameImportTX, network.layer1) const signingTxB = fundTransaction(txB, importerAddress, utxos, feeRate, 0) return signInputs(signingTxB, importerKey) }) ) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) } /** * Generates an announce transaction * @param {String} messageHash - the hash of the message to send. Should be * an already-announced zone file hash * @param {String | TransactionSigner} senderKeyIn - the private key * that pays for the transaction. Should be the key that owns the * name that the message recipients subscribe to * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * this function does not perform the requisite safety checks -- please see the * safety module for those. * @private */ function makeAnnounce(messageHash: string, senderKeyIn: string | TransactionSigner, buildIncomplete: boolean = false ) { const network = config.network const announceTX = makeAnnounceSkeleton(messageHash) const senderKey = getTransactionSigner(senderKeyIn) return senderKey.getAddress().then( senderAddress => Promise.all([network.getUTXOs(senderAddress), network.getFeeRate()]) .then(([utxos, feeRate]) => { const txB = TransactionBuilder.fromTransaction(announceTX, network.layer1) const signingTxB = fundTransaction(txB, senderAddress, utxos, feeRate, 0) return signInputs(signingTxB, senderKey) }) ) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) } /** * Generates a token-transfer transaction * @param {String} recipientAddress - the address to receive the tokens * @param {String} tokenType - the type of tokens to send * @param {Object} tokenAmount - the BigInteger encoding of an unsigned 64-bit number of * tokens to send * @param {String} scratchArea - an arbitrary string to include with the transaction * @param {String | TransactionSigner} senderKeyIn - the hex-encoded private key to send * the tokens * @param {String | TransactionSigner} btcFunderKeyIn - the hex-encoded private key to fund * the bitcoin fees for the transaction. Optional -- if not passed, will attempt to * fund with sender key. * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * This function does not perform the requisite safety checks -- please see the * safety module for those. * @private */ function makeTokenTransfer(recipientAddress: string, tokenType: string, tokenAmount: BN, scratchArea: string, senderKeyIn: string | TransactionSigner, btcFunderKeyIn?: string | TransactionSigner, buildIncomplete: boolean = false ) { const network = config.network const separateFunder = !!btcFunderKeyIn const senderKey = getTransactionSigner(senderKeyIn) const btcKey = btcFunderKeyIn ? getTransactionSigner(btcFunderKeyIn) : senderKey const txPromise = network.getConsensusHash() .then(consensusHash => makeTokenTransferSkeleton( recipientAddress, consensusHash, tokenType, tokenAmount, scratchArea)) return Promise.all([senderKey.getAddress(), btcKey.getAddress()]) .then(([senderAddress, btcAddress]) => { const btcUTXOsPromise = separateFunder ? network.getUTXOs(btcAddress) : Promise.resolve<UTXO[]>([]) return Promise.all([ network.getUTXOs(senderAddress), btcUTXOsPromise, network.getFeeRate(), txPromise ]).then(([senderUTXOs, btcUTXOs, feeRate, tokenTransferTX]) => { const txB = TransactionBuilder.fromTransaction(tokenTransferTX, network.layer1) if (separateFunder) { const payerInput = addOwnerInput(senderUTXOs, senderAddress, txB) const signingTxB = fundTransaction(txB, btcAddress, btcUTXOs, feeRate, payerInput.value) return signInputs(signingTxB, btcKey, [{ index: payerInput.index, signer: senderKey }]) } else { const signingTxB = fundTransaction(txB, senderAddress, senderUTXOs, feeRate, 0) return signInputs(signingTxB, senderKey) } }) }) .then(signingTxB => returnTransactionHex(signingTxB, buildIncomplete)) } /** * Generates a bitcoin spend to a specified address. This will fund up to `amount` * of satoshis from the payer's UTXOs. It will generate a change output if and only * if the amount of leftover change is *greater* than the additional fees associated * with the extra output. If the requested amount is not enough to fund the transaction's * associated fees, then this will reject with a InvalidAmountError * * UTXOs are selected largest to smallest, and UTXOs which cannot fund the fees associated * with their own input will not be included. * * If you specify an amount > the total balance of the payer address, then this will * generate a maximum spend transaction * * @param {String} destinationAddress - the address to receive the bitcoin payment * @param {String | TransactionSigner} paymentKeyIn - the private key * used to fund the bitcoin spend * @param {number} amount - the amount in satoshis for the payment address to * spend in this transaction * @param {boolean} buildIncomplete - optional boolean, defaults to false, * indicating whether the function should attempt to return an unsigned (or not fully signed) * transaction. Useful for passing around a TX for multi-sig input signing. * @returns {Promise} - a promise which resolves to the hex-encoded transaction. * @private */ function makeBitcoinSpend(destinationAddress: string, paymentKeyIn: string | TransactionSigner, amount: number, buildIncomplete: boolean = false ) { if (amount <= 0) { return Promise.reject(new InvalidParameterError('amount', 'amount must be greater than zero')) } const network = config.network const paymentKey = getTransactionSigner(paymentKeyIn) return paymentKey.getAddress().then( paymentAddress => Promise.all([network.getUTXOs(paymentAddress), network.getFeeRate()]) .then(([utxos, feeRate]) => { const txB = new TransactionBuilder(network.layer1) txB.setVersion(1) const destinationIndex = txB.addOutput(destinationAddress, 0) // will add utxos up to _amount_ and return the amount of leftover _change_ let change try { change = addUTXOsToFund(txB, utxos, amount, feeRate, false) } catch (err) { if (err.name === 'NotEnoughFundsError') { // actual amount funded = amount requested - remainder amount -= err.leftToFund change = 0 } else { throw err } } let feesToPay = feeRate * estimateTXBytes(txB, 0, 0) const feeForChange = feeRate * (estimateTXBytes(txB, 0, 1)) - feesToPay // it's worthwhile to add a change output if (change > feeForChange) {