syscoinjs-lib
Version:
A transaction creation library interfacing with coin selection for Syscoin.
1,389 lines (1,331 loc) • 60.7 kB
JavaScript
const axios = require('axios')
const BN = require('bn.js')
const BIP84 = require('bip84')
const CryptoJS = require('crypto-js')
const bjs = require('bitcoinjs-lib')
const bitcoinops = require('bitcoin-ops')
const varuint = require('varuint-bitcoin')
const { VerifyProof, GetProof } = require('eth-proof')
const { encode } = require('eth-util-lite')
const { Log } = require('eth-object')
const Web3 = require('web3')
const syscointx = require('syscointx-js')
const utxoLib = require('@trezor/utxo-lib')
const TrezorConnect = require('trezor-connect').default
const web3 = new Web3()
const bitcoinNetworks = { mainnet: bjs.networks.bitcoin, testnet: bjs.networks.testnet }
/* global localStorage */
const syscoinNetworks = {
mainnet: {
messagePrefix: '\x18Syscoin Signed Message:\n',
bech32: 'sys',
bip32: {
public: 0x0488b21e,
private: 0x0488ade4
},
pubKeyHash: 0x3f,
scriptHash: 0x05,
wif: 0x80
},
testnet: {
messagePrefix: '\x18Syscoin Signed Message:\n',
bech32: 'tsys',
bip32: {
public: 0x043587cf,
private: 0x04358394
},
pubKeyHash: 0x41,
scriptHash: 0xc4,
wif: 0xef
}
}
const bitcoinZPubTypes = { mainnet: { zprv: '04b2430c', zpub: '04b24746' }, testnet: { vprv: '045f18bc', vpub: '045f1cf6' } }
const bitcoinXPubTypes = { mainnet: { zprv: bitcoinNetworks.mainnet.bip32.private, zpub: bitcoinNetworks.mainnet.bip32.public }, testnet: { vprv: bitcoinNetworks.testnet.bip32.private, vpub: bitcoinNetworks.testnet.bip32.public } }
const syscoinZPubTypes = { mainnet: { zprv: '04b2430c', zpub: '04b24746' }, testnet: { vprv: '045f18bc', vpub: '045f1cf6' } }
const syscoinXPubTypes = { mainnet: { zprv: syscoinNetworks.mainnet.bip32.private, zpub: syscoinNetworks.mainnet.bip32.public }, testnet: { vprv: syscoinNetworks.testnet.bip32.private, vpub: syscoinNetworks.testnet.bip32.public } }
const syscoinSLIP44 = 57
const bitcoinSLIP44 = 0
let trezorInitialized = false
const DEFAULT_TREZOR_DOMAIN = 'https://connect.trezor.io/8/'
const VaultManager = '0x7904299b3D3dC1b03d1DdEb45E9fDF3576aCBd5f'
const tokenFreezeFunction = '0b8914e27c9a6c88836bc5547f82ccf331142c761f84e9f1d36934a6a31eefad' // token freeze function signature
const axiosConfig = {
withCredentials: true
}
/* fetchBackendAsset
Purpose: Fetch asset information from backend Blockbook provider
Param backendURL: Required. Fully qualified URL for blockbook
Param assetGuid: Required. Asset to fetch
Returns: Returns JSON object in response, asset information object in JSON
*/
async function fetchBackendAsset (backendURL, assetGuid) {
try {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
// eslint-disable-next-line no-undef
if (fetch) {
// eslint-disable-next-line no-undef
const response = await fetch(`${blockbookURL}/api/v2/asset/${assetGuid}?details=basic`)
if (response.ok) {
const data = await response.json()
if (data.asset) {
return data.asset
}
}
} else {
const request = await axios.get(blockbookURL + '/api/v2/asset/' + assetGuid + '?details=basic', axiosConfig)
if (request && request.data && request.data.asset) {
return request.data.asset
}
}
return null
} catch (e) {
return e
}
}
/* fetchBackendListAssets
Purpose: Fetch list of assets from backend Blockbook provider via a filter
Param backendURL: Required. Fully qualified URL for blockbook
Param filter: Required. Asset to fetch via filter, will filter contract or symbol fields
Returns: Returns JSON array in response, asset information objects in JSON
*/
async function fetchBackendListAssets (backendURL, filter) {
try {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
// eslint-disable-next-line no-undef
if (fetch) {
// eslint-disable-next-line no-undef
const request = await fetch(blockbookURL + '/api/v2/assets/' + filter)
const data = await request.json()
if (data && data.asset) {
return data.asset
}
} else {
const request = await axios.get(blockbookURL + '/api/v2/assets/' + filter, axiosConfig)
if (request && request.data && request.data.asset) {
return request.data.asset
}
}
return null
} catch (e) {
return e
}
}
/* fetchBackendSPVProof
Purpose: Fetch SPV Proof from backend Blockbook provider. To be used to create a proof for the NEVM bridge.
Param backendURL: Required. Fully qualified URL for blockbook
Param addressOrXpub: Required. An address or XPUB to fetch UTXO's for
Param options: Optional. Optional queries based on https://github.com/syscoin/blockbook/blob/master/docs/api.md#get-utxo
Returns: Returns JSON object in response, UTXO object array in JSON
*/
async function fetchBackendSPVProof (backendURL, txid) {
try {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
const url = blockbookURL + '/api/v2/getspvproof/' + txid
// eslint-disable-next-line no-undef
if (fetch) {
// eslint-disable-next-line no-undef
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
return data
}
} else {
const request = await axios.get(url, axiosConfig)
if (request && request.data) {
return request.data
}
}
return null
} catch (e) {
return e
}
}
/* fetchBackendUTXOS
Purpose: Fetch UTXO's for an address or XPUB from backend Blockbook provider
Param backendURL: Required. Fully qualified URL for blockbook
Param addressOrXpub: Required. An address or XPUB to fetch UTXO's for
Param options: Optional. Optional queries based on https://github.com/syscoin/blockbook/blob/master/docs/api.md#get-utxo
Returns: Returns JSON object in response, UTXO object array in JSON
*/
async function fetchBackendUTXOS (backendURL, addressOrXpub, options) {
try {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
let url = blockbookURL + '/api/v2/utxo/' + addressOrXpub
if (options) {
url += '?' + options
}
// eslint-disable-next-line no-undef
if (fetch) {
// eslint-disable-next-line no-undef
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
if (data) {
data.addressOrXpub = addressOrXpub
return data
}
}
} else {
const request = await axios.get(url, axiosConfig)
if (request && request.data) {
request.data.addressOrXpub = addressOrXpub
return request.data
}
}
return null
} catch (e) {
return e
}
}
/* fetchBackendAccount
Purpose: Fetch address or XPUB information including transactions and balance information (based on options) from backend Blockbook provider
Param backendURL: Required. Fully qualified URL for blockbook
Param addressOrXpub: Required. An address or XPUB to fetch UTXO's for
Param options: Optional. Optional queries based on https://github.com/syscoin/blockbook/blob/master/docs/api.md#get-xpub
Param xpub: Optional. If addressOrXpub is an XPUB set to true.
Param mySignerObj: Optional. Signer object if you wish to update change/receiving indexes from backend provider (and XPUB token information is provided in response)
Returns: Returns JSON object in response, account object in JSON
*/
async function fetchBackendAccount (backendURL, addressOrXpub, options, xpub, mySignerObj) {
try {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
let url = blockbookURL
if (xpub) {
url += '/api/v2/xpub/'
} else {
url += '/api/v2/address/'
}
url += addressOrXpub
if (options) {
url += '?' + options
}
// eslint-disable-next-line no-undef
if (fetch) {
// eslint-disable-next-line no-undef
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
if (xpub && data.tokens && mySignerObj) {
mySignerObj.setLatestIndexesFromXPubTokens(data.tokens)
}
data.addressOrXpub = addressOrXpub
return data
}
} else {
const request = await axios.get(url, axiosConfig)
if (request && request.data) {
// if fetching xpub data
if (xpub && request.data.tokens && mySignerObj) {
mySignerObj.setLatestIndexesFromXPubTokens(request.data.tokens)
}
return request.data
}
}
return null
} catch (e) {
console.log('Exception: ' + e.message)
return null
}
}
/* sendRawTransaction
Purpose: Send raw transaction to backend Blockbook provider to send to the network
Param backendURL: Required. Fully qualified URL for blockbook
Param txHex: Required. Raw transaction hex
Param mySignerObj: Optional. Signer object if you wish to update change/receiving indexes from backend provider through fetchBackendAccount()
Returns: Returns txid in response or error
*/
async function sendRawTransaction (backendURL, txHex, mySignerObj) {
try {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
// eslint-disable-next-line no-undef
if (fetch) {
const requestOptions = {
method: 'POST',
body: txHex
}
// eslint-disable-next-line no-undef
const response = await fetch(blockbookURL + '/api/v2/sendtx/', requestOptions)
if (response.ok) {
const data = await response.json()
if (mySignerObj) {
await fetchBackendAccount(blockbookURL, mySignerObj.getAccountXpub(), 'tokens=used&details=tokens', true, mySignerObj)
}
return data
}
} else {
const request = await axios.post(blockbookURL + '/api/v2/sendtx/', txHex, axiosConfig)
if (request && request.data) {
if (mySignerObj) {
await fetchBackendAccount(blockbookURL, mySignerObj.getAccountXpub(), 'tokens=used&details=tokens', true, mySignerObj)
}
return request.data
}
}
return null
} catch (e) {
return e
}
}
/* fetchBackendRawTx
Purpose: Get transaction from txid from backend Blockbook provider
Param backendURL: Required. Fully qualified URL for blockbook
Param txid: Required. Transaction ID to get information for
Returns: Returns JSON object in response, transaction object in JSON
*/
async function fetchBackendRawTx (backendURL, txid) {
try {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
// eslint-disable-next-line no-undef
if (fetch) {
// eslint-disable-next-line no-undef
const response = await fetch(blockbookURL + '/api/v2/tx/' + txid)
if (response.ok) {
const data = await response.json()
if (data) {
return data
}
}
} else {
const request = await axios.get(blockbookURL + '/api/v2/tx/' + txid, axiosConfig)
if (request && request.data) {
return request.data
}
}
return null
} catch (e) {
return e
}
}
/* fetchProviderInfo
Purpose: Get prover info including blockbook and backend data
Returns: Returns JSON object in response, provider object in JSON
*/
async function fetchProviderInfo (backendURL) {
try {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
// eslint-disable-next-line no-undef
if (fetch) {
// eslint-disable-next-line no-undef
const response = await fetch(blockbookURL + '/api/v2')
if (response.ok) {
const data = await response.json()
if (data) {
return data
}
}
} else {
const request = await axios.get(blockbookURL + '/api/v2', axiosConfig)
if (request && request.data) {
return request.data
}
}
return null
} catch (e) {
return e
}
}
/* fetchBackendBlock
Purpose: Get block from backend
Returns: Returns JSON object in response, block object in JSON
*/
async function fetchBackendBlock (backendURL, blockhash) {
try {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
// eslint-disable-next-line no-undef
if (fetch) {
// eslint-disable-next-line no-undef
const response = await fetch(blockbookURL + '/api/v2/block/' + blockhash)
if (response.ok) {
const data = await response.json()
if (data) {
return data
}
}
} else {
const request = await axios.get(blockbookURL + '/api/v2/block/' + blockhash, axiosConfig)
if (request && request.data) {
return request.data
}
}
return null
} catch (e) {
return e
}
}
/* fetchEstimateFee
Purpose: Get estimated fee from backend
Returns: Returns JSON object in response, fee object in JSON
Param blocks: Required. How many blocks to estimate fee for.
Param options: Optional. possible value conservative=true or false for conservative fee. Default is true.
Returns: Returns fee response in integer. Fee rate in satoshi per kilobytes.
*/
async function fetchEstimateFee (backendURL, blocks, options) {
try {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
let url = blockbookURL + '/api/v2/estimatefee/' + blocks
if (options) {
url += '?' + options
}
// eslint-disable-next-line no-undef
if (fetch) {
// eslint-disable-next-line no-undef
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
if (data && data.result) {
let feeInt = parseInt(data.result)
// if fee is 0 it usually means not enough data, so use min relay fee which is 1000 satoshi per kb in Core by default
if (feeInt <= 0) {
feeInt = 1000
}
return feeInt
}
}
} else {
const request = await axios.get(url, axiosConfig)
if (request && request.data && request.data.result) {
let feeInt = parseInt(request.data.result)
// if fee is 0 it usually means not enough data, so use min relay fee which is 1000 satoshi per kb in Core by default
if (feeInt <= 0) {
feeInt = 1000
}
return feeInt
}
}
return null
} catch (e) {
return e
}
}
/* signPSBTWithWIF
Purpose: Sign PSBT with WiF
Param psbt: Required. Partially signed transaction object
Param wif: Required. Private key in WIF format to sign inputs with
Param network: Required. bitcoinjs-lib Network object
Returns: psbt from bitcoinjs-lib
*/
async function signPSBTWithWIF (psbt, wif, network) {
const wifObject = bjs.ECPair.fromWIF(
wif,
network
)
// sign inputs with wif
await psbt.signAllInputsAsync(wifObject)
try {
if (psbt.validateSignaturesOfAllInputs()) {
psbt.finalizeAllInputs()
}
} catch (err) {
}
return psbt
}
/* signWithWIF
Purpose: Sign Result object with WiF
Param res: Required. The resulting object passed in which is assigned from syscointx.createTransaction()/syscointx.createAssetTransaction()
Param wif: Required. Private key in WIF format to sign inputs with, can be array of keys
Param network: Required. bitcoinjs-lib Network object
Returns: psbt from bitcoinjs-lib
*/
async function signWithWIF (psbt, wif, network) {
if (Array.isArray(wif)) {
for (const wifKey of wif) {
psbt = await signPSBTWithWIF(psbt, wifKey, network)
}
return psbt
} else {
return await signPSBTWithWIF(psbt, wif, network)
}
}
/* buildEthProof
Purpose: Build Ethereum SPV proof using eth-proof library
Param assetOpts: Required. Object containing web3url and ethtxid fields populated
Returns: Returns JSON object in response, SPV proof object in JSON
*/
async function buildEthProof (assetOpts) {
const ethProof = new GetProof(assetOpts.web3url)
const web3Provider = new Web3(assetOpts.web3url)
try {
const txHash = assetOpts.ethtxid.startsWith('0x') ? assetOpts.ethtxid : `0x${assetOpts.ethtxid}`
let result = await ethProof.transactionProof(txHash)
const txObj = await VerifyProof.getTxFromTxProofAt(result.txProof, result.txIndex)
const txvalue = txObj.hex.substring(2) // remove hex prefix
let destinationaddress
const txroot = result.header[4].toString('hex')
const txRootFromProof = VerifyProof.getRootFromProof(result.txProof)
if (txroot !== txRootFromProof.toString('hex')) {
throw new Error('TxRoot mismatch')
}
const txparentnodes = encode(result.txProof).toString('hex')
const txpath = encode(result.txIndex).toString('hex')
const blocknumber = parseInt(result.header[8].toString('hex'), 16)
const block = await web3Provider.eth.getBlock(blocknumber)
const blockhash = block.hash.substring(2) // remove hex prefix
const receiptroot = result.header[5].toString('hex')
result = await ethProof.receiptProof(txHash)
const txReceipt = await VerifyProof.getReceiptFromReceiptProofAt(result.receiptProof, result.txIndex)
const receiptRootFromProof = VerifyProof.getRootFromProof(result.receiptProof)
if (receiptroot !== receiptRootFromProof.toString('hex')) {
throw new Error('ReceiptRoot mismatch')
}
const receiptparentnodes = encode(result.receiptProof).toString('hex')
const blockHashFromHeader = VerifyProof.getBlockHashFromHeader(result.header)
if (blockhash !== blockHashFromHeader.toString('hex')) {
throw new Error('BlockHash mismatch')
}
const receiptvalue = txReceipt.hex.substring(2) // remove hex prefix
let amount = new web3.utils.BN(0)
let assetguid
for (let i = 0; i < txReceipt.setOfLogs.length; i++) {
const log = Log.fromRaw(txReceipt.setOfLogs[i]).toObject()
if (!log.topics || log.topics.length !== 3) {
continue
}
if (
log.topics[0].toString('hex').toLowerCase() === tokenFreezeFunction.toLowerCase() &&
log.address.toLowerCase() === VaultManager.toLowerCase()
) {
// Decode indexed parameters from topics
assetguid = web3.utils.hexToNumberString('0x' + log.topics[1].toString('hex'))
// Decode non-indexed parameters from data
const paramResults = web3.eth.abi.decodeParameters(
['uint', 'string'],
log.data
)
amount = web3.utils.toBN(paramResults[0]).toString()
destinationaddress = paramResults[1].trim()
break
}
}
const ethtxid = web3.utils.sha3(Buffer.from(txvalue, 'hex')).substring(2) // not txid but txhash of the tx object used for calculating tx commitment without requiring transaction deserialization
return { ethtxid, blockhash, assetguid, destinationaddress, amount, txvalue, txroot, txparentnodes, txpath, blocknumber, receiptvalue, receiptroot, receiptparentnodes }
} catch (e) {
console.log('Exception: ' + e.message)
return e
}
}
/* sanitizeBlockbookUTXOs
Purpose: Sanitize backend provider UTXO objects to be useful for this library
Param sysFromXpubOrAddress: Required. The XPUB or address that was called to fetch UTXOs
Param utxoObj: Required. Backend provider UTXO JSON object to be sanitized
Param network: Optional. Defaults to Syscoin Mainnet.
Param txOpts: Optional. If its passed in we use assetWhiteList field of options to skip over (if assetWhiteList is null) UTXO's
Param assetMap: Optional. Destination outputs for transaction requiring UTXO sanitizing, used in assetWhiteList check described above
Param excludeZeroConf: Optional. False by default. Filtering out 0 conf UTXO, new/update/send asset transactions must use confirmed inputs only as per Syscoin Core mempool policy
Returns: Returns sanitized UTXO object for use internally in this library
*/
function sanitizeBlockbookUTXOs (sysFromXpubOrAddress, utxoObj, network, txOpts, assetMap, excludeZeroConf) {
if (!txOpts) {
txOpts = { rbf: false }
}
const sanitizedUtxos = { utxos: [] }
if (Array.isArray(utxoObj)) {
utxoObj.utxos = utxoObj
}
if (utxoObj.assets) {
sanitizedUtxos.assets = new Map()
utxoObj.assets.forEach(asset => {
const assetObj = {}
if (asset.contract) {
asset.contract = asset.contract.replace(/^0x/, '')
assetObj.contract = Buffer.from(asset.contract, 'hex')
}
assetObj.maxsupply = new BN(asset.maxSupply)
assetObj.precision = asset.decimals
sanitizedUtxos.assets.set(asset.assetGuid, assetObj)
})
}
if (utxoObj.utxos) {
utxoObj.utxos.forEach(utxo => {
// xpub queries will return utxo.address and address queries should use sysFromXpubOrAddress as address is not provided
utxo.address = utxo.address || sysFromXpubOrAddress
if (excludeZeroConf && utxo.confirmations <= 0) {
return
}
const newUtxo = { type: 'LEGACY', address: utxo.address, txId: utxo.txid, path: utxo.path, vout: utxo.vout, value: new BN(utxo.value), locktime: utxo.locktime }
if (newUtxo.address.startsWith(network.bech32)) {
newUtxo.type = 'BECH32'
}
if (utxo.assetInfo) {
newUtxo.assetInfo = { assetGuid: utxo.assetInfo.assetGuid, value: new BN(utxo.assetInfo.value) }
const assetObj = sanitizedUtxos.assets.get(utxo.assetInfo.assetGuid)
// sanity check to ensure sanitizedUtxos.assets has all of the assets being added to UTXO that are assets
if (!assetObj) {
return
}
// not sending this asset (assetMap) and assetWhiteList option if set with this asset will skip this check, by default this check is done and inputs will be skipped
if ((!assetMap || !assetMap.has(utxo.assetInfo.assetGuid)) && (txOpts.assetWhiteList && !txOpts.assetWhiteList.has(utxo.assetInfo.assetGuid))) {
console.log('SKIPPING utxo')
return
}
}
sanitizedUtxos.utxos.push(newUtxo)
})
}
return sanitizedUtxos
}
/* getMemoFromScript
Purpose: Return memo from a script, null otherwise
Param script: Required. OP_RETURN script output
Param memoHeader: Required. Memo prefix, application specific
*/
function getMemoFromScript (script, memoHeader) {
const pos = script.indexOf(memoHeader)
if (pos >= 0) {
return script.slice(pos + memoHeader.length)
}
return null
}
/* getMemoFromOpReturn
Purpose: Return memo from an array of outputs by finding the OP_RETURN output and extracting the memo from the script, return null if not found
Param outputs: Required. Tx output array
Param memoHeader: Optional. Memo prefix, application specific. If not passed in just return the raw opreturn script if found.
*/
function getMemoFromOpReturn (outputs, memoHeader) {
for (let i = 0; i < outputs.length; i++) {
const output = outputs[i]
if (output.script) {
// find opreturn
const chunks = bjs.script.decompile(output.script)
if (chunks[0] === bitcoinops.OP_RETURN) {
if (memoHeader) {
return getMemoFromScript(chunks[1], memoHeader)
} else {
return chunks[1]
}
}
}
}
return null
}
/* getAllocationsFromTx
Purpose: Return allocation information for an asset transaction. Pass through to syscointx-js
Param tx: Required. bitcoinjs transaction
*/
function getAllocationsFromTx (tx) {
return syscointx.getAllocationsFromTx(tx) || []
}
/* isBech32
Purpose: Return a boolean if a given sys address is a bech32 address
Param address: Required. Address to check
*/
function isBech32 (address) {
try {
utxoLib.address.fromBech32(address)
return true
} catch (e) {
return false
}
}
/* isScriptHash
Purpose: Return a boolean if a given sys address is a script hash accordingly to the syscoinNetwork selected
Param address: Required. Address to verify
Param networkInfo: Required. Network information to verify
*/
function isScriptHash (address, networkInfo) {
if (!isBech32(address)) {
const decoded = utxoLib.address.fromBase58Check(address)
if (decoded.version === networkInfo.pubKeyHash) {
return false
}
if (decoded.version === networkInfo.scriptHash) {
return true
}
} else {
const decoded = utxoLib.address.fromBech32(address)
if (decoded.data.length === 20) {
return false
}
if (decoded.data.length === 32) {
return true
}
}
throw new Error('isScriptHash: Unknown address type')
};
/* convertToAddressNFormat
Purpose: Return path in addressN format
Param path: Required derivation path
*/
function convertToAddressNFormat (path) {
const pathArray = path.replace(/'/g, '').split('/')
pathArray.shift()
const addressN = []
for (const index in pathArray) {
if (Number(index) <= 2 && Number(index) >= 0) {
addressN[Number(index)] = Number(pathArray[index]) | 0x80000000
} else {
addressN[Number(index)] = Number(pathArray[index])
}
}
return addressN
};
/* setTransactionMemo
Purpose: Return transaction with memo appended to the inside of the OP_RETURN output, return null if not found
Param rawHex: Required. Raw transaction hex
Param memoHeader: Required. Memo prefix, application specific
Param buffMemo: Required. Buffer memo to put into the transaction
*/
function setTransactionMemo (rawHex, memoHeader, buffMemo) {
const txn = bjs.Transaction.fromHex(rawHex)
let processed = false
if (!buffMemo) {
return txn
}
for (let key = 0; key < txn.outs.length; key++) {
const out = txn.outs[key]
const chunksIn = bjs.script.decompile(out.script)
if (chunksIn[0] !== bjs.opcodes.OP_RETURN) {
continue
}
txn.outs.splice(key, 1)
const updatedData = [chunksIn[1], memoHeader, buffMemo]
txn.addOutput(bjs.payments.embed({ data: [Buffer.concat(updatedData)] }).output, 0)
processed = true
break
}
if (processed) {
const memoRet = getMemoFromOpReturn(txn.outs, memoHeader)
if (!memoRet || !memoRet.equals(buffMemo)) {
return null
}
return txn
}
const updatedData = [memoHeader, buffMemo]
txn.addOutput(bjs.payments.embed({ data: [Buffer.concat(updatedData)] }).output, 0)
const memoRet = getMemoFromOpReturn(txn.outs, memoHeader)
if (!memoRet || !memoRet.equals(buffMemo)) {
return null
}
return txn
}
function setPoDA (bjstx, blobData) {
if (!blobData) {
return
}
for (let key = 0; key < bjstx.outs.length; key++) {
const out = bjstx.outs[key]
const chunksIn = bjs.script.decompile(out.script)
if (chunksIn[0] !== bjs.opcodes.OP_RETURN) {
continue
}
bjstx.outs.splice(key, 1)
const updatedData = [chunksIn[1], blobData]
bjstx.addOutput(bjs.payments.embed({ data: [Buffer.concat(updatedData)] }).output, 0)
}
}
function copyPSBT (psbt, networkIn, outputIndexToModify, outputScript) {
const psbtNew = new bjs.Psbt({ network: networkIn })
psbtNew.setVersion(psbt.version)
const txInputs = psbt.txInputs
for (let i = 0; i < txInputs.length; i++) {
const input = txInputs[i]
const dataInput = psbt.data.inputs[i]
const inputObj = {
hash: input.hash,
index: input.index,
sequence: input.sequence,
bip32Derivation: dataInput.bip32Derivation || []
}
if (dataInput.nonWitnessUtxo) {
inputObj.nonWitnessUtxo = dataInput.nonWitnessUtxo
} else if (dataInput.witnessUtxo) {
inputObj.witnessUtxo = dataInput.witnessUtxo
}
psbtNew.addInput(inputObj)
dataInput.unknownKeyVals.forEach(unknownKeyVal => {
psbtNew.addUnknownKeyValToInput(i, unknownKeyVal)
})
}
const txOutputs = psbt.txOutputs
for (let i = 0; i < txOutputs.length; i++) {
const output = txOutputs[i]
if (i === outputIndexToModify) {
psbtNew.addOutput({
script: outputScript,
address: outputScript,
value: output.value
})
} else {
psbtNew.addOutput(output)
}
}
return psbtNew
}
/* HDSigner
Purpose: Manage HD wallet and accounts, connects to SyscoinJS object
Param mnemonic: Required. Bip32 seed phrase
Param password: Optional. Encryption password for local storage on web clients
Param isTestnet: Optional. Is using testnet network?
Param networks: Optional. Defaults to Syscoin network. bitcoinjs-lib network settings for coin being used.
Param SLIP44: Optional. SLIP44 value for the coin, see: https://github.com/satoshilabs/slips/blob/master/slip-0044.md
Param pubTypes: Optional. Defaults to Syscoin ZPub/VPub types. Specific ZPub for bip84 and VPub for testnet
*/
function Signer (password, isTestnet, networks, SLIP44, pubTypes) {
this.isTestnet = isTestnet || false
this.networks = networks || syscoinNetworks
this.password = password
this.SLIP44 = this.isTestnet ? 1 : SLIP44 || syscoinSLIP44 // 1 is testnet for all coins,
if (!this.isTestnet) {
this.network = this.networks.mainnet || syscoinNetworks.mainnet
} else {
this.network = this.networks.testnet || syscoinNetworks.testnet
}
this.pubTypes = pubTypes || syscoinZPubTypes
this.accounts = [] // length serialized
this.changeIndex = -1
this.receivingIndex = -1
this.accountIndex = 0
this.setIndexFlag = 0
}
function TrezorSigner (password, isTestnet, networks, SLIP44, pubTypes, connectSrc, disableLazyLoad) {
try {
if (!trezorInitialized) {
connectSrc = connectSrc || DEFAULT_TREZOR_DOMAIN
const lazyLoad = !disableLazyLoad
TrezorConnect.init({
connectSrc: connectSrc,
lazyLoad: lazyLoad, // this param will prevent iframe injection until TrezorConnect.method will be called
manifest: {
email: 'sidhujag@syscoin.org',
appUrl: 'https://syscoin.org/'
}
})
trezorInitialized = true // Trezor should be initialized on first run only
}
} catch (e) {
throw new Error('TrezorSigner should be called only from browser context: ' + e)
}
this.Signer = new Signer(password, isTestnet, networks, SLIP44, pubTypes)
this.restore(this.Signer.password)
}
function HDSigner (mnemonic, password, isTestnet, networks, SLIP44, pubTypes, bipNum) {
this.Signer = new Signer(password, isTestnet, networks, SLIP44, pubTypes)
this.mnemonic = mnemonic // serialized
/* eslint new-cap: ["error", { "newIsCap": false }] */
this.fromMnemonic = new BIP84.fromMnemonic(mnemonic, this.Signer.password, this.Signer.isTestnet, this.Signer.SLIP44, this.Signer.pubTypes, this.Signer.network)
// try to restore, if it does not succeed then initialize from scratch
if (!this.Signer.password || !this.restore(this.Signer.password, bipNum)) {
this.createAccount(bipNum)
}
}
/* signPSBT
Purpose: Sign PSBT with XPUB information from HDSigner
Param psbt: Required. Partially signed transaction object
Param pathIn: Optional. Custom HD Bip32 path useful if signing from a specific address like a multisig
Returns: psbt from bitcoinjs-lib
*/
HDSigner.prototype.signPSBT = async function (psbt, pathIn) {
const txInputs = psbt.txInputs
const fp = this.getMasterFingerprint()
for (let i = 0; i < txInputs.length; i++) {
const dataInput = psbt.data.inputs[i]
if (pathIn || (dataInput.unknownKeyVals && dataInput.unknownKeyVals.length > 1 && dataInput.unknownKeyVals[1].key.equals(Buffer.from('path')) && (!dataInput.bip32Derivation || dataInput.bip32Derivation.length === 0))) {
const path = pathIn || dataInput.unknownKeyVals[1].value.toString()
const pubkey = this.derivePubKey(path)
const address = this.getAddressFromPubKey(pubkey)
if (pubkey && (pathIn || dataInput.unknownKeyVals[0].value.toString() === address)) {
dataInput.bip32Derivation = [
{
masterFingerprint: fp,
path: path,
pubkey: pubkey
}]
}
}
}
await psbt.signAllInputsHDAsync(this.getRootNode())
try {
if (psbt.validateSignaturesOfAllInputs()) {
psbt.finalizeAllInputs()
}
} catch (err) {
}
return psbt
}
/* convertToTrezorFormat
Purpose: Convert syscoin PSBT to Trezor format
Param psbt: Required. Partially signed transaction object
Returns: trezor params to signTransaction
*/
TrezorSigner.prototype.convertToTrezorFormat = function (psbt, pathIn) {
const trezortx = {}
const coin = this.Signer.SLIP44 === syscoinSLIP44 ? 'sys' : 'btc'
trezortx.coin = coin
trezortx.version = psbt.version
trezortx.inputs = []
trezortx.outputs = []
for (let i = 0; i < psbt.txInputs.length; i++) {
const scriptTypes = psbt.getInputType(i)
const input = psbt.txInputs[i]
const inputItem = {}
inputItem.prev_index = input.index
inputItem.prev_hash = input.hash.reverse().toString('hex')
if (input.sequence) inputItem.sequence = input.sequence
const dataInput = psbt.data.inputs[i]
let path = ''
if (pathIn || (dataInput.unknownKeyVals && dataInput.unknownKeyVals.length > 1 &&
dataInput.unknownKeyVals[1].key.equals(Buffer.from('path')) &&
(!dataInput.bip32Derivation || dataInput.bip32Derivation.length === 0))) {
path = pathIn || (dataInput.unknownKeyVals[1].value.toString())
inputItem.address_n = convertToAddressNFormat(path)
}
switch (scriptTypes) {
case 'multisig':
inputItem.script_type = 'SPENDMULTISIG'
break
case 'witnesspubkeyhash':
inputItem.script_type = 'SPENDWITNESS'
break
default:
inputItem.script_type = isP2WSHScript(psbt.data.inputs[i].witnessUtxo.script) ? 'SPENDP2SHWITNESS' : 'SPENDADDRESS'
break
}
trezortx.inputs.push(inputItem)
}
for (let i = 0; i < psbt.txOutputs.length; i++) {
const output = psbt.txOutputs[i]
const outputItem = {}
const chunks = bjs.script.decompile(output.script)
outputItem.amount = output.value.toString()
if (chunks[0] === bitcoinops.OP_RETURN) {
outputItem.script_type = 'PAYTOOPRETURN'
outputItem.op_return_data = chunks[1].toString('hex')
} else {
if (isBech32(output.address)) {
if (output.script.length === 34 &&
output.script[0] === 0 &&
output.script[1] === 0x20) {
outputItem.script_type = 'PAYTOP2SHWITNESS'
} else {
outputItem.script_type = 'PAYTOWITNESS'
}
} else {
outputItem.script_type = isScriptHash(output.address, this.network) ? 'PAYTOSCRIPTHASH' : 'PAYTOADDRESS'
}
outputItem.address = output.address
}
trezortx.outputs.push(outputItem)
}
return trezortx
}
/* sign
Purpose: Create signing information based on Trezor format
Param psbt: Required. PSBT object from bitcoinjs-lib
Returns: trezortx or txid
*/
TrezorSigner.prototype.sign = async function (psbt, pathIn) {
if (psbt.txInputs.length <= 0 || psbt.txOutputs.length <= 0 || psbt.version === undefined) {
throw new Error('PSBT object is lacking information')
}
const trezorTx = this.convertToTrezorFormat(psbt, pathIn)
const response = await TrezorConnect.signTransaction(trezorTx)
if (response.success === true) {
const tx = bjs.Transaction.fromHex(response.payload.serializedTx)
for (let i = 0; i < psbt.data.inputs.length; i++) {
if (tx.ins[i].witness === (undefined || null)) {
throw new Error('Please move your funds to a Segwit address: https://wiki.trezor.io/Account')
}
const partialSig = [
{
pubkey: tx.ins[i].witness[1],
signature: tx.ins[i].witness[0]
}
]
psbt.updateInput(i, { partialSig })
}
try {
if (psbt.validateSignaturesOfAllInputs()) {
psbt.finalizeAllInputs()
}
} catch (err) {
console.log(err)
}
return psbt
} else {
throw new Error('Trezor sign failed: ' + response.payload.error)
}
}
/* sign
Purpose: Create signing information based on HDSigner (if set) and call signPSBT() to actually sign
Param psbt: Required. PSBT object from bitcoinjs-lib
Returns: psbt from bitcoinjs-lib
*/
HDSigner.prototype.sign = async function (psbt, pathIn) {
return await this.signPSBT(psbt, pathIn)
}
/* getMasterFingerprint
Purpose: Get master seed fingerprint used for signing with bitcoinjs-lib PSBT's
Returns: bip32 root master fingerprint
*/
HDSigner.prototype.getMasterFingerprint = function () {
return bjs.bip32.fromSeed(this.fromMnemonic.seed, this.Signer.network).fingerprint
}
/* deriveAccount
Purpose: Derive HD account based on index number passed in
Param index: Required. Account number to derive
Param bipNum: Optional. BIP number to use for derivation
Returns: bip32 node for derived account
*/
TrezorSigner.prototype.deriveAccount = async function (index, bipNum) {
if (bipNum === undefined) {
bipNum = 44
}
if (this.Signer.pubTypes === syscoinZPubTypes ||
this.Signer.pubTypes === bitcoinZPubTypes) {
bipNum = 84
}
const coin = this.Signer.SLIP44 === syscoinSLIP44 ? 'sys' : 'btc'
const keypath = 'm/' + bipNum + "'/" + this.Signer.SLIP44 + "'/" + index + "'"
if (this.Signer.isTestnet) {
throw new Error(
'Cant use TrezorSigner on testnet .'
)
}
return new Promise((resolve, reject) => {
TrezorConnect.getAccountInfo({
path: keypath,
coin: coin
})
.then((response) => {
if (response.success) {
resolve(response.payload)
}
reject(response.payload.error)
})
.catch((error) => {
console.error('TrezorConnectError', error)
reject(error)
})
})
}
HDSigner.prototype.deriveAccount = function (index, bipNum) {
if (bipNum === undefined) {
bipNum = 44
}
if (this.Signer.pubTypes === syscoinZPubTypes ||
this.Signer.pubTypes === bitcoinZPubTypes) {
bipNum = 84
}
return this.fromMnemonic.deriveAccount(index, bipNum)
}
/* setAccountIndex
Purpose: Set HD account based on accountIndex number passed in so HD indexes (change/receiving) will be updated accordingly to this account
Param accountIndex: Required. Account number to use
*/
Signer.prototype.setAccountIndex = function (accountIndex) {
if (accountIndex > this.accounts.length) {
console.log('Account does not exist, use createAccount to create it first...')
return
}
if (this.accountIndex !== accountIndex) {
this.changeIndex = -1
this.receivingIndex = -1
this.accountIndex = accountIndex
}
}
TrezorSigner.prototype.setAccountIndex = function (accountIndex) {
this.Signer.setAccountIndex(accountIndex)
}
HDSigner.prototype.setAccountIndex = function (accountIndex) {
this.Signer.setAccountIndex(accountIndex)
}
/* restore
Purpose: Restore on load from local storage and decrypt data to de-serialize objects
Param password: Required. Decryption password to unlock seed phrase
Returns: boolean on success for fail of restore
*/
TrezorSigner.prototype.restore = function (password) {
let browserStorage = (typeof localStorage === 'undefined' || localStorage === null) ? null : localStorage
if (!browserStorage) {
const LocalStorage = require('node-localstorage').LocalStorage
browserStorage = new LocalStorage('./scratch')
}
const key = this.Signer.network.bech32 + '_trezorsigner'
const ciphertext = browserStorage.getItem(key)
if (ciphertext === null) {
return false
}
const bytes = CryptoJS.AES.decrypt(ciphertext, password)
if (!bytes || bytes.length === 0) {
return false
}
const decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8))
const numAccounts = decryptedData.numAccounts
// sanity checks
if (this.Signer.accountIndex > 1000) {
return false
}
this.Signer.changeIndex = -1
this.Signer.receivingIndex = -1
this.Signer.accountIndex = 0
for (let i = 0; i < numAccounts; i++) {
this.Signer.accounts.push(new BIP84.fromZPub(decryptedData.xpubArr[i], this.Signer.pubTypes, this.Signer.networks))
if (this.Signer.accounts[i].getAccountPublicKey() !== decryptedData.xpubArr[i]) {
throw new Error('Account public key mismatch,check pubtypes and networks being used')
}
}
return true
}
HDSigner.prototype.restore = function (password, bipNum) {
let browserStorage = (typeof localStorage === 'undefined' || localStorage === null) ? null : localStorage
if (!browserStorage) {
const LocalStorage = require('node-localstorage').LocalStorage
browserStorage = new LocalStorage('./scratch')
}
const key = this.Signer.network.bech32 + '_hdsigner'
const ciphertext = browserStorage.getItem(key)
if (ciphertext === null) {
return false
}
const bytes = CryptoJS.AES.decrypt(ciphertext, password)
if (!bytes || bytes.length === 0) {
return false
}
const decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8))
this.mnemonic = decryptedData.mnemonic
const numAccounts = decryptedData.numAccounts
// sanity checks
if (this.Signer.accountIndex > 1000) {
return false
}
this.Signer.accounts = []
this.Signer.changeIndex = -1
this.Signer.receivingIndex = -1
this.Signer.accountIndex = 0
for (let i = 0; i < numAccounts; i++) {
const child = this.deriveAccount(i, bipNum)
/* eslint new-cap: ["error", { "newIsCap": false }] */
this.Signer.accounts.push(new BIP84.fromZPrv(child, this.Signer.pubTypes, this.Signer.networks))
}
return this
}
/* backup
Purpose: Encrypt to password and backup to local storage for persistence
*/
TrezorSigner.prototype.backup = function () {
let browserStorage = (typeof localStorage === 'undefined' || localStorage === null) ? null : localStorage
if (!this.Signer.password) { return }
if (!browserStorage) {
const LocalStorage = require('node-localstorage').LocalStorage
browserStorage = new LocalStorage('./scratch')
}
const key = this.Signer.network.bech32 + '_trezorsigner'
const xpubs = []
for (let i = 0; i < this.Signer.accounts.length; i++) {
xpubs[i] = this.Signer.accounts[i].getAccountPublicKey()
}
const obj = { xpubArr: xpubs, numAccounts: this.Signer.accounts.length }
const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(obj), this.Signer.password).toString()
browserStorage.setItem(key, ciphertext)
}
HDSigner.prototype.backup = function () {
let browserStorage = (typeof localStorage === 'undefined' || localStorage === null) ? null : localStorage
if (!this.Signer.password) { return }
if (!browserStorage) {
const LocalStorage = require('node-localstorage').LocalStorage
browserStorage = new LocalStorage('./scratch')
}
const key = this.Signer.network.bech32 + '_hdsigner'
const obj = { mnemonic: this.mnemonic, numAccounts: this.Signer.accounts.length }
const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(obj), this.Signer.password).toString()
browserStorage.setItem(key, ciphertext)
}
/* getNewChangeAddress
Purpose: Get new address for sending change to
Param skipIncrement: Optional. If we should not count the internal change index counter (if you want to get the same change address in the future)
Param bipNum: Optional. If you want the address derivated in regard of an specific bip number
Returns: string address used for change outputs
*/
Signer.prototype.getNewChangeAddress = async function (skipIncrement, bipNum) {
if (this.changeIndex === -1 && this.blockbookURL) {
let res = await fetchBackendAccount(this.blockbookURL, this.getAccountXpub(), 'tokens=used&details=tokens', true, this)
if (res === null) {
// try once more in case it fails for some reason
res = await fetchBackendAccount(this.blockbookURL, this.getAccountXpub(), 'tokens=used&details=tokens', true, this)
if (res === null) {
throw new Error('Could not update XPUB change index')
}
}
}
const address = this.createAddress(this.changeIndex + 1, true, bipNum)
if (address) {
if (!skipIncrement) {
this.changeIndex++
}
return address
}
return null
}
TrezorSigner.prototype.getNewChangeAddress = async function (skipIncrement, bipNum) {
return this.Signer.getNewChangeAddress(skipIncrement, bipNum)
}
HDSigner.prototype.getNewChangeAddress = async function (skipIncrement, bipNum) {
return this.Signer.getNewChangeAddress(skipIncrement, bipNum)
}
/* getNewReceivingAddress
Purpose: Get new address for sending coins to
Param skipIncrement: Optional. If we should not count the internal receiving index counter (if you want to get the same address in the future)
Param bipNum: Optional. If you want the address derivated in regard of an specific bip number
Returns: string address used for receiving outputs
*/
Signer.prototype.getNewReceivingAddress = async function (skipIncrement, bipNum) {
if (this.receivingIndex === -1 && this.blockbookURL) {
let res = await fetchBackendAccount(this.blockbookURL, this.getAccountXpub(), 'tokens=used&details=tokens', true, this)
if (res === null) {
// try once more in case it fails for some reason
res = await fetchBackendAccount(this.blockbookURL, this.getAccountXpub(), 'tokens=used&details=tokens', true, this)
if (res === null) {
throw new Error('Could not update XPUB receiving index')
}
}
}
const address = this.createAddress(this.receivingIndex + 1, false, bipNum)
if (address) {
if (!skipIncrement) {
this.receivingIndex++
}
return address
}
return null
}
TrezorSigner.prototype.getNewReceivingAddress = async function (skipIncrement, bipNum) {
return this.Signer.getNewReceivingAddress(skipIncrement, bipNum)
}
HDSigner.prototype.getNewReceivingAddress = async function (skipIncrement, bipNum) {
return this.Signer.getNewReceivingAddress(skipIncrement, bipNum)
}
/* createAccount
Purpose: Create and derive a new account
Param bipNum: Optional. If you want the address derivated in regard of an specific bip number
Returns: Account index of new account
*/
TrezorSigner.prototype.createAccount = async function (bipNum) {
this.Signer.changeIndex = -1
this.Signer.receivingIndex = -1
return new Promise((resolve, reject) => {
this.deriveAccount(this.Signer.accounts.length, bipNum).then(child => {
this.Signer.accountIndex = this.Signer.accounts.length
this.Signer.accounts.push(new BIP84.fromZPub(child.descriptor, this.Signer.pubTypes, this.Signer.networks))
this.backup()
resolve(this.Signer.accountIndex)
}).catch(err => {
console.error(err)
reject(err)
}
)
})
}
HDSigner.prototype.createAccount = function (bipNum) {
this.Signer.changeIndex = -1
this.Signer.receivingIndex = -1
const child = this.deriveAccount(this.Signer.accounts.length, bipNum)
this.Signer.accountIndex = this.Signer.accounts.length
/* eslint new-cap: ["error", { "newIsCap": false }] */
this.Signer.accounts.push(new BIP84.fromZPrv(child, this.Signer.pubTypes, this.Signer.networks))
this.backup()
return this.Signer.accountIndex
}
/* getAccountXpub
Purpose: Get XPUB for account, useful for public provider lookups based on XPUB accounts
Returns: string representing hex XPUB
*/
Signer.prototype.getAccountXpub = function () {
return this.accounts[this.accountIndex].getAccountPublicKey()
}
TrezorSigner.prototype.getAccountXpub = function () {
return this.Signer.getAccountXpub()
}
HDSigner.prototype.getAccountXpub = function () {
return this.Signer.getAccountXpub()
}
/* setLatestIndexesFromXPubTokens
Purpose: Sets the change and receiving indexes from XPUB tokens passed in, from a backend provider response
Param tokens: Required. XPUB tokens from provider response to XPUB account details.
*/
Signer.prototype.setLatestIndexesFromXPubTokens = function (tokens) {
this.setIndexFlag++
// concurrency check make sure you don't execute this logic while it is already running as signer state is updated here
// also in case there is some bug in the code that prevents it ever from being called because this.setIndexFlag = 0 doesn't happen we
// stop worrying about the flag after it reached 100 attempts
if (this.setIndexFlag > 1 && this.setIndexFlag < 100) {
return
}
if (tokens) {
tokens.forEach(token => {
if (!token.transfers || !token.path) {
return
}
const transfers = parseInt(token.transfers, 10)
if (token.path && transfers > 0) {
const splitPath = token.path.split('/')
if (splitPath.length >= 6) {
const change = parseInt(splitPath[4], 10)
const index = parseInt(splitPath[5], 10)
if (change === 1) {
if (index > this.changeIndex) {
this.changeIndex = index
}
} else if (index > this.receivingIndex) {
this.receivingIndex = index
}
}
}
})
}
this.setIndexFlag = 0
}
TrezorSigner.prototype.setLatestIndexesFromXPubTokens = function (tokens) {
this.Signer.setLatestIndexesFromXPubTokens(tokens)
}
HDSigner.prototype.setLatestIndexesFromXPubTokens = function (tokens) {
this.Signer.setLatestIndexesFromXPubTokens(tokens)
}
Signer.prototype.createAddress = function (addressIndex, isChange, bipNum) {
if (bipNum === undefined) {
bipNum = 44
}
if (this.pubTypes === syscoinZPubTypes ||
this.pubTypes === bitcoinZPubTypes) {
bipNum = 84
}
return this.accounts[this.accountIndex].getAddress(addressIndex, isChange, bipNum)
}
TrezorSigner.prototype.createAddress = function (addressIndex, isChange, bipNum) {
return this.Signer.createAddress(addressIndex, isChange, bipNum)
}
HDSigner.prototype.createAddress = function (addressIndex, isChange, bipNum) {
return this.Signer.createAddress(addressIndex, isChange, bipNum)
}
/* createKeypair
Purpose: Sets the change and receiving indexes from XPUB tokens passed in, from a backend provider response
Param addressIndex: Optional. HD path address index. If not provided uses the stored change/recv indexes for the last path prefix
Param isC