syscointx-js
Version:
A transaction creation library interfacing with coin selection for Syscoin.
948 lines (878 loc) • 32.4 kB
JavaScript
const BN = require('bn.js')
const ext = require('./bn-extensions')
const utils = require('./utils')
const syscoinBufferUtils = require('./bufferutilsassets.js')
const bitcoin = require('bitcoinjs-lib')
const coinSelect = require('coinselectsyscoin')
const bitcoinops = require('bitcoin-ops')
function createTransaction (txOpts, utxos, changeAddress, outputsArr, feeRate, inputsArr) {
let dataBuffer = null
let totalMemoLen = 0
let totalBlobLen = 0
if (txOpts.memo) {
if (!txOpts.memoHeader) {
console.log('No Memo header defined')
return {
error: 'INVALID_MEMO',
message: 'No Memo header defined'
}
}
totalMemoLen = txOpts.memo.length + txOpts.memoHeader.length
}
if (totalMemoLen > 80) {
console.log('Memo too big! Max is 80 bytes, found: ' + totalMemoLen)
return {
error: 'INVALID_MEMO',
message: 'Memo too big! Max is 80 bytes, found: ' + totalMemoLen
}
}
if (txOpts.memo) {
dataBuffer = Buffer.concat([txOpts.memoHeader, txOpts.memo])
}
let txVersion = 2
inputsArr = inputsArr || []
if (txOpts.blobHash) {
if (!txOpts.blobData) {
console.log('blobHash provided but no blobData in txOptions')
return {
error: 'INVALID_BLOB',
message: 'blobHash provided but no blobData in txOptions'
}
}
totalBlobLen = txOpts.blobData.length
txVersion = utils.SYSCOIN_TX_VERSION_NEVM_DATA
dataBuffer = syscoinBufferUtils.serializePoDA({ blobHash: txOpts.blobHash })
}
let res = coinSelect.coinSelect(utxos.utxos, inputsArr, outputsArr, feeRate, txVersion, totalMemoLen, totalBlobLen)
if (!res.inputs && !res.outputs) {
if (txOpts.blobHash) {
console.log('createTransaction: inputs or outputs are empty after coinSelect creating blob')
// Return the complete error object from coinselect
return res
}
const assetAllocations = []
console.log('createTransaction: inputs or outputs are empty after coinSelect trying to fund with Syscoin Asset inputs...')
res = coinSelect.coinSelectAssetGas(assetAllocations, utxos.utxos, inputsArr, outputsArr, feeRate, utils.SYSCOIN_TX_VERSION_ALLOCATION_SEND, utxos.assets, null)
if (!res.inputs || !res.outputs) {
console.log('createTransaction: inputs or outputs are empty after coinSelectAssetGas')
// Return the complete error object from coinselect
return res
}
if (assetAllocations.length > 0) {
txVersion = utils.SYSCOIN_TX_VERSION_ALLOCATION_SEND
// re-use syscoin change outputs for allocation change outputs where we can, this will possible remove one output and save fees
optimizeOutputs(res.outputs, assetAllocations)
const assetAllocationsBuffer = syscoinBufferUtils.serializeAssetAllocations(assetAllocations)
let buffArr
if (dataBuffer) {
buffArr = [assetAllocationsBuffer, dataBuffer]
} else {
buffArr = [assetAllocationsBuffer]
}
// create and add data script for OP_RETURN
const dataScript = bitcoin.payments.embed({ data: [Buffer.concat(buffArr)] }).output
const dataOutput = {
script: dataScript,
value: ext.BN_ZERO
}
res.outputs.push(dataOutput)
}
} else if (dataBuffer) {
const updatedData = [dataBuffer]
const dataScript = bitcoin.payments.embed({ data: [Buffer.concat(updatedData)] }).output
const dataOutput = {
script: dataScript,
value: ext.BN_ZERO
}
res.outputs.push(dataOutput)
}
const inputs = res.inputs
const outputs = res.outputs
optimizeFees(txVersion, inputs, outputs, feeRate)
if (txVersion === utils.SYSCOIN_TX_VERSION_ALLOCATION_SEND) {
// ensure ZDAG is only enable for transactions <= 1100 bytes
const bytesAccum = coinSelect.utils.transactionBytes(inputs, outputs)
// if size too large we ensure ZDAG isn't set by enabling RBF (disable ZDAG)
if (bytesAccum > 1100) {
if (!txOpts.rbf) {
txOpts.rbf = true
}
}
}
if (txOpts.rbf) {
inputs.forEach(input => {
input.sequence = utils.MAX_BIP125_RBF_SEQUENCE
})
}
outputs.forEach(output => {
// watch out, outputs may have been added that you need to provide
// an output address/script for
if (!output.address) {
output.address = changeAddress
}
})
// Get the actual fee and size
const bytesAccum = coinSelect.utils.transactionBytes(inputs, outputs)
return {
success: true,
txVersion,
inputs,
outputs,
fee: res.fee,
feeRate,
size: bytesAccum
}
}
// update all allocations at some index or higher
function updateAllocationIndexes (assetAllocations, index) {
assetAllocations.forEach(voutAsset => {
voutAsset.values.forEach(output => {
if (output.n > index) {
output.n--
}
})
})
}
function optimizeOutputs (outputs, assetAllocations) {
// first find all syscoin outputs that are change (should only be one)
const changeOutputs = outputs.filter(output => output.changeIndex !== undefined)
if (changeOutputs.length > 1) {
console.log('optimizeOutputs: too many change outputs')
return
}
// find all asset change outputs
const assetChangeOutputs = outputs.filter(assetOutput => assetOutput.assetChangeIndex !== undefined && assetOutput.assetInfo.assetGuid > 0)
changeOutputs.forEach(output => {
// for every asset output and find any where the allocation index and change output index don't match
// make the allocation point to the syscoin change output and we can delete the asset output (it sends dust anyway)
for (let i = 0; i < assetChangeOutputs.length; i++) {
const assetOutput = assetChangeOutputs[i]
// get the allocation by looking up from assetChangeIndex which is indexing into the allocations array for this asset guid
const allocations = assetAllocations.find(voutAsset => voutAsset.assetGuid === assetOutput.assetInfo.assetGuid)
const allocation = allocations.values[assetOutput.assetChangeIndex]
// ensure that the output index's don't match between sys change and asset output
if (allocation.n !== output.changeIndex) {
// remove the output, we will recalc and optimize fees after this call
outputs.splice(allocation.n, 1)
// because we deleted this index, it will invalidate any indexes after (we must subtract by one on every index after assetChangeIndex)
updateAllocationIndexes(assetAllocations, allocation.n)
// set them the same and remove asset output
// we reduce index by one because any index > allocation.n would have been reduced by updateAllocationIndexes and so changeIndex should also by reduced by 1 if its above allocation.n
if (output.changeIndex > allocation.n) {
allocation.n = output.changeIndex - 1
} else {
allocation.n = output.changeIndex
}
// add assetInfo to output as its a sys change output which now becomes asset output as well (only needed for further calls which check assetInfo on outputs, not for signing or verifying the transaction)
outputs[allocation.n].assetInfo = assetOutput.assetInfo
// clear change address as it should use sys change address instead (when adding outputs)
allocation.changeAddress = null
return
}
}
})
}
function optimizeFees (txVersion, inputs, outputs, feeRate) {
const changeOutputs = outputs.filter(output => output.changeIndex !== undefined)
if (changeOutputs.length > 1) {
console.log('optimizeFees: too many change outputs')
return
}
if (changeOutputs.length === 0) {
console.log('optimizeFees: no change outputs')
return
}
const changeOutput = changeOutputs[0]
const bytesAccum = coinSelect.utils.transactionBytes(inputs, outputs)
const feeRequired = ext.mul(feeRate, bytesAccum)
let feeFoundInOut = ext.sub(coinSelect.utils.sumOrNaN(inputs), coinSelect.utils.sumOrNaN(outputs))
// first output of burn to sys is not accounted for with inputs, its minted based on sysx asset output to burn
if (txVersion === utils.SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_SYSCOIN) {
feeFoundInOut = ext.add(feeFoundInOut, outputs[0].value)
}
if (feeFoundInOut && ext.gt(feeFoundInOut, feeRequired)) {
const reduceFee = ext.sub(feeFoundInOut, feeRequired)
console.log('optimizeFees: reducing fees by: ' + reduceFee.toNumber())
// add to change to effectively reduce fee
changeOutput.value = ext.add(changeOutput.value, reduceFee)
} else if (ext.lt(feeFoundInOut, feeRequired)) {
console.log('optimizeFees: warning, not enough fees found in transaction: required: ' + feeRequired.toNumber() + ' found: ' + feeFoundInOut.toNumber())
}
}
function getAllocationsFromOutputs (outputs) {
let opReturnScript = null
for (let i = 0; i < outputs.length; i++) {
const output = outputs[i]
if (!output.script) {
continue
}
// find opreturn
const chunks = bitcoin.script.decompile(output.script)
if (chunks[0] === bitcoinops.OP_RETURN) {
opReturnScript = chunks[1]
break
}
}
if (opReturnScript === null) {
console.log('no OPRETURN script found')
return null
}
const allocation = syscoinBufferUtils.deserializeAssetAllocations(Buffer.isBuffer(opReturnScript) ? opReturnScript : Buffer.from(opReturnScript))
if (!allocation) {
return null
}
return allocation
}
function getAllocationsFromTx (tx) {
if (!utils.isSyscoinTx(tx.version)) {
return null
}
return getAllocationsFromOutputs(tx.outs)
}
function getPoDAFromOutputs (outputs) {
let opReturnScript = null
for (let i = 0; i < outputs.length; i++) {
const output = outputs[i]
if (!output.script) {
continue
}
// find opreturn
const chunks = bitcoin.script.decompile(output.script)
if (chunks[0] === bitcoinops.OP_RETURN) {
opReturnScript = chunks[1]
break
}
}
if (opReturnScript === null) {
console.log('no OPRETURN script found')
return null
}
const blob = syscoinBufferUtils.deserializePoDA(Buffer.isBuffer(opReturnScript) ? opReturnScript : Buffer.from(opReturnScript))
if (!blob) {
return null
}
return blob
}
function getPoDAFromTx (tx) {
if (!utils.isPoDATx(tx.version)) {
return null
}
return getPoDAFromOutputs(tx.outs)
}
function getAssetsFromOutputs (outputs) {
const allocation = getAllocationsFromOutputs(outputs)
if (!allocation) {
return null
}
const assets = new Map()
allocation.forEach(assetAllocation => {
assets.set(assetAllocation.assetGuid, {})
})
return assets
}
// get all assets found in an asset tx returned in a map of assets keyed by asset guid
function getAssetsFromTx (tx) {
const allocation = getAllocationsFromTx(tx)
if (!allocation) {
return null
}
const assets = new Map()
allocation.forEach(assetAllocation => {
assets.set(assetAllocation.assetGuid, {})
})
return assets
}
function createAssetTransaction (txVersion, txOpts, utxos, dataBuffer, dataAmount, assetMap, sysChangeAddress, feeRate) {
const assetSelectResult = coinSelect.coinSelectAsset(utxos.utxos, assetMap, feeRate, txVersion)
let { inputs, outputs, assetAllocations } = assetSelectResult
// .inputs and .outputs will be undefined if no solution was found
if (!inputs || !outputs) {
console.log('createAssetTransaction: inputs or outputs are empty after coinSelectAsset')
// Return the complete error object from coinselect
return assetSelectResult
}
let burnAllocationValue
if (utils.isAllocationBurn(txVersion)) {
// ensure only 1 to 2 outputs (2 if change was required)
if (outputs.length > 2 && outputs.length < 1) {
console.log('Assetallocationburn: expect output of length 1 got: ' + outputs.length)
return {
error: 'INVALID_OUTPUT_COUNT',
message: 'Assetallocationburn: expect output of length 1 got: ' + outputs.length
}
}
const assetAllocation = assetAllocations.find(voutAsset => voutAsset.assetGuid === outputs[0].assetInfo.assetGuid)
if (assetAllocation === undefined) {
console.log('Assetallocationburn: assetAllocations map does not have key: ' + outputs[0].assetInfo.assetGuid)
return {
error: 'INVALID_ASSET_ALLOCATION',
message: 'Assetallocationburn: assetAllocations map does not have key: ' + outputs[0].assetInfo.assetGuid
}
}
burnAllocationValue = new BN(assetAllocation.values[0].value)
// remove first output if there is more than one
// it will be size of 1 if you burn the exact right amount you own (ie utxo has 5 sysx and you burn 5 sysx)
// it will be size of 2 if you burn less than what you own (ie utxo has 5 sysx and you burn 4 sysx, 1 sysx should be change)
if (outputs.length > 1) {
outputs.splice(0, 1)
// we removed the first index via slice above, so all N's at index 1 or above should be reduced by 1
updateAllocationIndexes(assetAllocations, 0)
} else {
outputs[0].assetChangeIndex = undefined
}
// point first allocation to next output (burn output)
assetAllocation.values[0].n = outputs.length
}
let assetAllocationsBuffer = syscoinBufferUtils.serializeAssetAllocations(assetAllocations)
let buffArr
if (dataBuffer) {
buffArr = [assetAllocationsBuffer, dataBuffer]
} else {
buffArr = [assetAllocationsBuffer]
}
// create and add data script for OP_RETURN
let dataScript = bitcoin.payments.embed({ data: [Buffer.concat(buffArr)] }).output
const dataOutput = {
script: dataScript,
value: dataAmount
}
outputs.push(dataOutput)
const res = coinSelect.coinSelectAssetGas(assetAllocations, utxos.utxos, inputs, outputs, feeRate, txVersion, utxos.assets, assetMap)
if (!res.inputs || !res.outputs) {
console.log('createAssetTransaction: inputs or outputs are empty after coinSelectAssetGas')
// Return the complete error object from coinselect
return res
}
inputs = res.inputs
outputs = res.outputs
// once funded we should swap the first output asset amount to sys amount as we are burning sysx to sys in output 0
if (utils.isAllocationBurn(txVersion)) {
if (txVersion === utils.SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_SYSCOIN) {
// modify output from asset value to syscoin value
// first output is special it is the sys amount being minted
outputs[0].value = burnAllocationValue
}
}
// optimizeOutputs reorganizes outputs and we need to ensure we don't do this with burntosyscoin since its assumed first output has the sys value we need to create
if (txVersion !== utils.SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_SYSCOIN) {
// re-use syscoin change outputs for allocation change outputs where we can, this will possible remove one output and save fees
optimizeOutputs(outputs, assetAllocations)
}
// serialize allocations again they may have been changed in optimization
assetAllocationsBuffer = syscoinBufferUtils.serializeAssetAllocations(assetAllocations)
if (dataBuffer) {
buffArr = [assetAllocationsBuffer, dataBuffer]
} else {
buffArr = [assetAllocationsBuffer]
}
// update script with new guid
dataScript = bitcoin.payments.embed({ data: [Buffer.concat(buffArr)] }).output
// update output with new data output with new guid
outputs.forEach(output => {
if (output.script) {
output.script = dataScript
}
})
optimizeFees(txVersion, inputs, outputs, feeRate)
if (txVersion === utils.SYSCOIN_TX_VERSION_ALLOCATION_SEND) {
// ensure ZDAG is only enable for transactions <= 1100 bytes
const bytesAccum = coinSelect.utils.transactionBytes(inputs, outputs)
// if size too large we ensure ZDAG isn't set by enabling RBF (disable ZDAG)
if (bytesAccum > 1100) {
if (!txOpts.rbf) {
txOpts.rbf = true
}
}
}
if (txOpts.rbf) {
inputs.forEach(input => {
input.sequence = utils.MAX_BIP125_RBF_SEQUENCE
})
}
outputs.forEach(output => {
// watch out, outputs may have been added that you need to provide
// an output address/script for
if (!output.address) {
if (output.assetInfo) {
if (assetMap.has(output.assetInfo.assetGuid)) {
const changeAddress = assetMap.get(output.assetInfo.assetGuid).changeAddress
if (changeAddress) {
output.address = changeAddress
}
}
}
}
// if we still don't have address set to sys change address
if (!output.address) {
output.address = sysChangeAddress
}
})
// Get the actual fee and size
const bytesAccum = coinSelect.utils.transactionBytes(inputs, outputs)
return {
success: true,
txVersion,
inputs,
outputs,
fee: res.fee,
feeRate,
size: bytesAccum
}
}
function assetAllocationSend (txOpts, utxos, assetMap, sysChangeAddress, feeRate) {
const txVersion = utils.SYSCOIN_TX_VERSION_ALLOCATION_SEND
const dataAmount = ext.BN_ZERO
let dataBuffer = null
if (txOpts.memo) {
if (!Buffer.isBuffer(txOpts.memo)) {
console.log('Memo must be Buffer object')
return {
error: 'INVALID_MEMO',
message: 'Memo must be Buffer object'
}
}
const totalLen = txOpts.memo.length + txOpts.memoHeader.length
if (!txOpts.memoHeader) {
console.log('No Memo header defined')
return {
error: 'INVALID_MEMO',
message: 'No Memo header defined'
}
}
if (totalLen > 80) {
console.log('Memo too big! Max is 80 bytes, found: ' + totalLen)
return {
error: 'INVALID_MEMO',
message: 'Memo too big! Max is 80 bytes, found: ' + totalLen
}
}
dataBuffer = Buffer.concat([txOpts.memoHeader, txOpts.memo])
}
return createAssetTransaction(txVersion, txOpts, utxos, dataBuffer, dataAmount, assetMap, sysChangeAddress, feeRate)
}
function assetAllocationBurn (assetOpts, txOpts, utxos, assetMap, sysChangeAddress, feeRate) {
let txVersion = 0
if (assetOpts.ethaddress.length > 0) {
txVersion = utils.SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_ETHEREUM
} else {
txVersion = utils.SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_SYSCOIN
}
const dataAmount = ext.BN_ZERO
const dataBuffer = syscoinBufferUtils.serializeAllocationBurn(assetOpts)
return createAssetTransaction(txVersion, txOpts, utxos, dataBuffer, dataAmount, assetMap, sysChangeAddress, feeRate)
}
function assetAllocationMint (assetOpts, txOpts, utxos, assetMap, sysChangeAddress, feeRate) {
const txVersion = utils.SYSCOIN_TX_VERSION_ALLOCATION_MINT
const dataAmount = ext.BN_ZERO
if (assetOpts.txparentnodes.length > utils.USHRT_MAX()) {
console.log('tx parent nodes exceeds maximum allowable size of: ', utils.USHRT_MAX(), '. Found size: ', assetOpts.txparentnodes.length)
return {
error: 'INVALID_PARENT_NODES',
message: 'tx parent nodes exceeds maximum allowable size of: ' + utils.USHRT_MAX() + '. Found size: ' + assetOpts.txparentnodes.length
}
}
if (assetOpts.receiptparentnodes.length > utils.USHRT_MAX()) {
console.log('receipt parent nodes exceeds maximum allowable size of: ', utils.USHRT_MAX(), '. Found size: ', assetOpts.receiptparentnodes.length)
return {
error: 'INVALID_PARENT_NODES',
message: 'receipt parent nodes exceeds maximum allowable size of: ' + utils.USHRT_MAX() + '. Found size: ' + assetOpts.receiptparentnodes.length
}
}
// find byte offset of tx data in the parent nodes
assetOpts.txpos = assetOpts.txparentnodes.indexOf(assetOpts.txvalue)
if (assetOpts.txpos === -1) {
console.log('Could not find tx value in tx parent nodes')
return {
error: 'INVALID_TX_VALUE',
message: 'Could not find tx value in tx parent nodes'
}
}
// find byte offset of receipt data in the parent nodes
assetOpts.receiptpos = assetOpts.receiptparentnodes.indexOf(assetOpts.receiptvalue)
if (assetOpts.receiptpos === -1) {
console.log('Could not find receipt value in receipt parent nodes')
return {
error: 'INVALID_RECEIPT_VALUE',
message: 'Could not find receipt value in receipt parent nodes'
}
}
const dataBuffer = syscoinBufferUtils.serializeMintSyscoin(assetOpts)
return createAssetTransaction(txVersion, txOpts, utxos, dataBuffer, dataAmount, assetMap, sysChangeAddress, feeRate)
}
function syscoinBurnToAssetAllocation (txOpts, utxos, assetMap, sysChangeAddress, feeRate) {
const txVersion = utils.SYSCOIN_TX_VERSION_SYSCOIN_BURN_TO_ALLOCATION
const dataBuffer = null
let dataAmount = ext.BN_ZERO
const valueAssetObj = assetMap.values().next().value
if (valueAssetObj.outputs.length > 0) {
dataAmount = valueAssetObj.outputs[0].value
}
return createAssetTransaction(txVersion, txOpts, utxos, dataBuffer, dataAmount, assetMap, sysChangeAddress, feeRate)
}
function createPoDA (txOpts, utxos, sysChangeAddress, feeRate) {
if (!txOpts.blobData || !txOpts.blobHash) {
console.log('Could not find blob txOpt fields, cannot create PoDA transaction')
return {
error: 'INVALID_BLOB',
message: 'Could not find blob txOpt fields, cannot create PoDA transaction'
}
}
return createTransaction(txOpts, utxos, sysChangeAddress, [], feeRate)
}
function decodeRawTransaction (tx, network) {
const decoded = {
txid: tx.getId(),
hash: tx.getHash().toString('hex'),
version: tx.version,
size: tx.byteLength(),
vsize: tx.virtualSize(),
weight: tx.weight(),
locktime: tx.locktime,
vin: [],
vout: [],
syscoin: null
}
// Decode inputs
tx.ins.forEach((input, index) => {
const scriptBuf = Buffer.isBuffer(input.script) ? input.script : Buffer.from(input.script || [])
const vin = {
txid: Buffer.from(input.hash).reverse().toString('hex'),
vout: input.index,
scriptSig: {
asm: bitcoin.script.toASM(scriptBuf),
hex: scriptBuf.toString('hex')
},
sequence: input.sequence
}
// Add witness data if present
if (input.witness && input.witness.length > 0) {
vin.txinwitness = input.witness.map(w => w.toString('hex'))
}
decoded.vin.push(vin)
})
// Decode outputs
tx.outs.forEach((output, index) => {
const scriptBuf = Buffer.isBuffer(output.script) ? output.script : Buffer.from(output.script || [])
const vout = {
value: (typeof output.value === 'bigint' ? Number(output.value) : output.value) / 100000000, // Convert satoshis to coins
n: index,
scriptPubKey: {
asm: bitcoin.script.toASM(scriptBuf),
hex: scriptBuf.toString('hex'),
type: getOutputType(scriptBuf),
reqSigs: getRequiredSigs(scriptBuf),
addresses: getOutputAddresses(scriptBuf, network)
}
}
decoded.vout.push(vout)
})
// Decode Syscoin-specific data
decoded.syscoin = decodeSyscoinData(tx)
return decoded
}
function getOutputType (script) {
try {
const chunks = bitcoin.script.decompile(script)
if (!chunks) return 'nonstandard'
// OP_RETURN
if (chunks[0] === bitcoinops.OP_RETURN) {
return 'nulldata'
}
// P2PKH
if (chunks.length === 5 &&
chunks[0] === bitcoinops.OP_DUP &&
chunks[1] === bitcoinops.OP_HASH160 &&
Buffer.isBuffer(chunks[2]) &&
chunks[2].length === 20 &&
chunks[3] === bitcoinops.OP_EQUALVERIFY &&
chunks[4] === bitcoinops.OP_CHECKSIG) {
return 'pubkeyhash'
}
// P2SH
if (chunks.length === 3 &&
chunks[0] === bitcoinops.OP_HASH160 &&
Buffer.isBuffer(chunks[1]) &&
chunks[1].length === 20 &&
chunks[2] === bitcoinops.OP_EQUAL) {
return 'scripthash'
}
// P2WPKH
if (chunks.length === 2 &&
chunks[0] === bitcoinops.OP_0 &&
Buffer.isBuffer(chunks[1]) &&
chunks[1].length === 20) {
return 'witness_v0_keyhash'
}
// P2WSH
if (chunks.length === 2 &&
chunks[0] === bitcoinops.OP_0 &&
Buffer.isBuffer(chunks[1]) &&
chunks[1].length === 32) {
return 'witness_v0_scripthash'
}
// P2TR (Taproot)
if (chunks.length === 2 &&
chunks[0] === bitcoinops.OP_1 &&
Buffer.isBuffer(chunks[1]) &&
chunks[1].length === 32) {
return 'witness_v1_taproot'
}
// P2PK
if (chunks.length === 2 &&
Buffer.isBuffer(chunks[0]) &&
(chunks[0].length === 33 || chunks[0].length === 65) &&
chunks[1] === bitcoinops.OP_CHECKSIG) {
return 'pubkey'
}
// Multisig
if (chunks.length >= 4 &&
chunks[chunks.length - 1] === bitcoinops.OP_CHECKMULTISIG) {
return 'multisig'
}
return 'nonstandard'
} catch (error) {
return 'nonstandard'
}
}
function getRequiredSigs (script) {
try {
const chunks = bitcoin.script.decompile(script)
if (!chunks) return null
// Multisig
if (chunks.length >= 4 &&
chunks[chunks.length - 1] === bitcoinops.OP_CHECKMULTISIG) {
const m = chunks[0]
// Handle OP_1 through OP_16 opcodes
if (typeof m === 'number') {
if (m >= bitcoinops.OP_1 && m <= bitcoinops.OP_16) {
return m - bitcoinops.OP_1 + 1
} else if (m >= 1 && m <= 16) {
return m
}
}
}
// Standard single sig types
const type = getOutputType(script)
if (['pubkeyhash', 'scripthash', 'witness_v0_keyhash', 'witness_v0_scripthash', 'witness_v1_taproot', 'pubkey'].includes(type)) {
return 1
}
return null
} catch (error) {
return null
}
}
function getOutputAddresses (script, network) {
try {
const chunks = bitcoin.script.decompile(script)
if (!chunks) return []
const type = getOutputType(script)
const targetNetwork = network || utils.syscoinNetworks.mainnet
switch (type) {
case 'pubkeyhash':
return [bitcoin.address.fromOutputScript(script, targetNetwork)]
case 'scripthash':
return [bitcoin.address.fromOutputScript(script, targetNetwork)]
case 'witness_v0_keyhash':
return [bitcoin.address.fromOutputScript(script, targetNetwork)]
case 'witness_v0_scripthash':
return [bitcoin.address.fromOutputScript(script, targetNetwork)]
case 'witness_v1_taproot':
return [bitcoin.address.fromOutputScript(script, targetNetwork)]
case 'multisig': {
const addresses = []
for (let i = 1; i < chunks.length - 2; i++) {
if (Buffer.isBuffer(chunks[i])) {
try {
const pubkey = chunks[i]
const hash = bitcoin.crypto.hash160(pubkey)
addresses.push(bitcoin.address.toBase58Check(hash, targetNetwork.pubKeyHash))
} catch (e) {
// Skip invalid pubkeys
}
}
}
return addresses
}
default:
return []
}
} catch (error) {
return []
}
}
function decodeSyscoinData (tx) {
const syscoinData = {
txtype: getSyscoinTxType(tx.version),
version: tx.version,
allocations: null,
burn: null,
mint: null,
poda: null
}
try {
// Decode asset allocations
if (utils.isAssetAllocationTx(tx.version)) {
const allocations = getAllocationsFromTx(tx)
if (allocations) {
syscoinData.allocations = {
assets: allocations.map(allocation => ({
assetGuid: allocation.assetGuid,
values: allocation.values.map(value => ({
n: value.n,
value: value.value.toString(),
valueFormatted: (value.value.toNumber() / 100000000).toFixed(8)
}))
}))
}
}
}
// Decode allocation burn data
if (utils.isAllocationBurn(tx.version)) {
const burnData = decodeBurnData(tx)
if (burnData) {
syscoinData.burn = burnData
}
}
// Decode mint data
if (tx.version === utils.SYSCOIN_TX_VERSION_ALLOCATION_MINT) {
const mintData = decodeMintData(tx)
if (mintData) {
syscoinData.mint = mintData
}
}
// Decode PoDA data
if (utils.isPoDATx(tx.version)) {
const podaData = getPoDAFromTx(tx)
if (podaData) {
syscoinData.poda = {
blobHash: podaData.blobHash.toString('hex'),
blobData: podaData.blobData ? podaData.blobData.toString('hex') : null
}
}
}
return syscoinData
} catch (error) {
console.log('Error decoding Syscoin data:', error)
return syscoinData
}
}
function getSyscoinTxType (version) {
switch (version) {
case utils.SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_SYSCOIN:
return 'assetallocationburn_to_syscoin'
case utils.SYSCOIN_TX_VERSION_SYSCOIN_BURN_TO_ALLOCATION:
return 'syscoinburn_to_allocation'
case utils.SYSCOIN_TX_VERSION_ALLOCATION_MINT:
return 'assetallocation_mint'
case utils.SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_ETHEREUM:
return 'assetallocationburn_to_ethereum'
case utils.SYSCOIN_TX_VERSION_ALLOCATION_SEND:
return 'assetallocation_send'
case utils.SYSCOIN_TX_VERSION_NEVM_DATA:
return 'nevm_data'
default:
return version === 1 || version === 2 ? 'bitcoin' : 'unknown'
}
}
function decodeBurnData (tx) {
try {
let opReturnScript = null
for (let i = 0; i < tx.outs.length; i++) {
const output = tx.outs[i]
const chunks = bitcoin.script.decompile(output.script)
if (chunks && chunks[0] === bitcoinops.OP_RETURN && chunks[1]) {
opReturnScript = chunks[1]
break
}
}
if (!opReturnScript) {
return null
}
const burnPayload = Buffer.isBuffer(opReturnScript) ? opReturnScript : Buffer.from(opReturnScript)
const burnData = syscoinBufferUtils.deserializeAllocationBurn(burnPayload, true)
if (!burnData) {
return null
}
return {
ethaddress: burnData.ethaddress.toString('hex'),
allocation: burnData.allocation
? burnData.allocation.map(allocation => ({
assetGuid: allocation.assetGuid,
values: allocation.values.map(value => ({
n: value.n,
value: value.value.toString(),
valueFormatted: (value.value.toNumber() / 100000000).toFixed(8)
}))
}))
: null
}
} catch (error) {
console.log('Error decoding burn data:', error)
return null
}
}
function decodeMintData (tx) {
try {
let opReturnScript = null
for (let i = 0; i < tx.outs.length; i++) {
const output = tx.outs[i]
const chunks = bitcoin.script.decompile(output.script)
if (chunks && chunks[0] === bitcoinops.OP_RETURN && chunks[1]) {
opReturnScript = chunks[1]
break
}
}
if (!opReturnScript) {
return null
}
const mintData = syscoinBufferUtils.deserializeMintSyscoin(Buffer.isBuffer(opReturnScript) ? opReturnScript : Buffer.from(opReturnScript))
if (!mintData) {
return null
}
return {
allocation: mintData.allocation
? mintData.allocation.map(allocation => ({
assetGuid: allocation.assetGuid,
values: allocation.values.map(value => ({
n: value.n,
value: value.value.toString(),
valueFormatted: (value.value.toNumber() / 100000000).toFixed(8)
}))
}))
: null,
ethtxid: mintData.ethtxid.toString('hex'),
blockhash: mintData.blockhash.toString('hex'),
txpos: mintData.txpos,
txparentnodes: mintData.txparentnodes.toString('hex'),
txpath: mintData.txpath.toString('hex'),
receiptpos: mintData.receiptpos,
receiptparentnodes: mintData.receiptparentnodes.toString('hex'),
txroot: mintData.txroot.toString('hex'),
receiptroot: mintData.receiptroot.toString('hex')
}
} catch (error) {
console.log('Error decoding mint data:', error)
return null
}
}
module.exports = {
utils,
coinSelect,
bufferUtils: syscoinBufferUtils,
createTransaction,
createAssetTransaction,
createPoDA,
assetAllocationSend,
assetAllocationBurn,
assetAllocationMint,
syscoinBurnToAssetAllocation,
getAssetsFromTx,
getAllocationsFromTx,
getAllocationsFromOutputs,
getPoDAFromOutputs,
getPoDAFromTx,
getAssetsFromOutputs,
decodeRawTransaction,
decodeSyscoinData,
getSyscoinTxType
}