forked from Atomicwallet/coinselect
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathblackjack.js
182 lines (174 loc) · 8.41 KB
/
blackjack.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
const utils = require('./utils')
const ext = require('./bn-extensions')
const BN = require('bn.js')
// only add inputs if they don't bust the target value (aka, exact match)
// worst-case: O(n)
function blackjack (utxos, inputs, outputs, feeRate, assets, txVersion, memoSize, blobSize) {
if (!utils.uintOrNull(feeRate)) return {}
const changeOutputBytes = utils.outputBytes({})
let memoPadding = 0
if (memoSize) {
memoPadding = memoSize + 5 + 8 // opreturn overhead + memo size + amount int64
}
blobSize = blobSize || 0
let feeBytes = new BN(changeOutputBytes.toNumber() + 4)
let bytesAccum = utils.transactionBytes(inputs, outputs)
let inAccum = utils.sumOrNaN(inputs)
let outAccum = utils.sumOrNaN(outputs, txVersion)
const memBytes = new BN(memoPadding)
let blobBytes = new BN(blobSize)
// factor blobs by 100x in fee market
blobBytes = ext.mul(blobBytes, new BN(0.01))
bytesAccum = ext.add(bytesAccum, memBytes)
feeBytes = ext.add(feeBytes, memBytes)
feeBytes = ext.add(feeBytes, blobBytes)
let fee = ext.mul(feeRate, bytesAccum)
const dustAmount = utils.dustThreshold({ type: 'BECH32' }, feeRate)
if (blobSize) {
outAccum = ext.add(outAccum, dustAmount)
bytesAccum = ext.add(bytesAccum, changeOutputBytes)
feeBytes = ext.add(feeBytes, changeOutputBytes)
// double up to be safe
bytesAccum = ext.add(bytesAccum, changeOutputBytes)
feeBytes = ext.add(feeBytes, changeOutputBytes)
}
// is already enough input?
if (ext.gte(inAccum, ext.add(outAccum, fee))) return utils.finalize(inputs, outputs, feeRate, changeOutputBytes)
const threshold = utils.dustThreshold({}, feeRate)
for (let i = 0; i < utxos.length; i++) {
const input = utxos[i]
const inputBytes = utils.inputBytes(input)
fee = ext.mul(feeRate, ext.add(bytesAccum, inputBytes))
const inputValue = utils.uintOrNull(input.value)
// would it waste value?
if (ext.gt(ext.add(inAccum, inputValue), ext.add(outAccum, fee, threshold))) continue
bytesAccum = ext.add(bytesAccum, inputBytes)
inAccum = ext.add(inAccum, inputValue)
inputs.push(input)
// if this is an asset input, we will need another output to send asset to so add dust satoshi to output and add output fee
if (input.assetInfo) {
const baseAssetID = utils.getBaseAssetID(input.assetInfo.assetGuid)
outAccum = ext.add(outAccum, dustAmount)
bytesAccum = ext.add(bytesAccum, utils.outputBytes({ type: 'BECH32' }))
// double up to be safe
bytesAccum = ext.add(bytesAccum, changeOutputBytes)
feeBytes = ext.add(feeBytes, changeOutputBytes)
// add another bech32 output for OP_RETURN overhead
// any extra data should be optimized out later as OP_RETURN is serialized and fees are optimized
bytesAccum = ext.add(bytesAccum, utils.outputBytes({ type: 'BECH32' }))
fee = ext.mul(feeRate, bytesAccum)
if (utils.isAssetAllocationTx(txVersion) && assets && assets.has(baseAssetID)) {
const utxoAssetObj = assets.get(baseAssetID)
// auxfee for this asset exists add another output
if (txVersion === utils.SYSCOIN_TX_VERSION_ALLOCATION_SEND && baseAssetID === input.assetInfo.assetGuid && utxoAssetObj.auxfeedetails && utxoAssetObj.auxfeedetails.auxfeeaddress && utxoAssetObj.auxfeedetails.auxfees && utxoAssetObj.auxfeedetails.auxfees.length > 0) {
outAccum = ext.add(outAccum, dustAmount)
bytesAccum = ext.add(bytesAccum, changeOutputBytes)
feeBytes = ext.add(feeBytes, changeOutputBytes)
// add another bech32 output for OP_RETURN overhead
// any extra data should be optimized out later as OP_RETURN is serialized and fees are optimized
bytesAccum = ext.add(bytesAccum, changeOutputBytes)
feeBytes = ext.add(feeBytes, changeOutputBytes)
}
// add bytes and fees for notary signature
if (utxoAssetObj.notarykeyid && utxoAssetObj.notarykeyid.length > 0) {
const sigBytes = new BN(65)
bytesAccum = ext.add(bytesAccum, sigBytes)
feeBytes = ext.add(feeBytes, sigBytes)
}
}
}
// go again?
if (ext.lt(inAccum, ext.add(outAccum, fee))) continue
return utils.finalize(inputs, outputs, feeRate, feeBytes)
}
return { fee: ext.mul(feeRate, bytesAccum) }
}
// average-case: O(n*log(n))
function blackjackAsset (utxos, assetMap, feeRate, txVersion, assets) {
if (!utils.uintOrNull(feeRate)) return {}
const isAsset = utils.isAsset(txVersion)
const isNonAssetFunded = utils.isNonAssetFunded(txVersion)
const dustAmount = utils.dustThreshold({ type: 'BECH32' }, feeRate)
const mapAssetAmounts = new Map()
const inputs = []
const outputs = []
const assetAllocations = []
let auxfeeValue = ext.BN_ZERO
for (let i = 0; i < utxos.length; i++) {
const input = utxos[i]
if (!input.assetInfo) {
continue
}
mapAssetAmounts.set(input.assetInfo.assetGuid + '-' + input.assetInfo.value.toString(10), i)
}
// loop through all assets looking to get funded, sort the utxo's and then try to fund them incrementally
for (const [assetGuid, valueAssetObj] of assetMap.entries()) {
const baseAssetID = utils.getBaseAssetID(assetGuid)
const utxoAssetObj = (assets && assets.get(baseAssetID)) || {}
const assetAllocation = { assetGuid: assetGuid, values: [], notarysig: utxoAssetObj.notarysig || Buffer.from('') }
if (!isAsset) {
// auxfee is set and its an allocation send
if (txVersion === utils.SYSCOIN_TX_VERSION_ALLOCATION_SEND && baseAssetID === assetGuid && utxoAssetObj.auxfeedetails && utxoAssetObj.auxfeedetails.auxfeeaddress && utxoAssetObj.auxfeedetails.auxfees && utxoAssetObj.auxfeedetails.auxfees.length > 0) {
let totalAssetValue = ext.BN_ZERO
// find total amount for this asset from assetMap
valueAssetObj.outputs.forEach(output => {
totalAssetValue = ext.add(totalAssetValue, output.value)
})
// get auxfee based on auxfee table and total amount sending
auxfeeValue = utils.getAuxFee(utxoAssetObj.auxfeedetails, totalAssetValue)
if (auxfeeValue.gt(ext.BN_ZERO)) {
assetAllocation.values.push({ n: outputs.length, value: auxfeeValue })
outputs.push({ address: utxoAssetObj.auxfeedetails.auxfeeaddress, type: 'BECH32', assetInfo: { assetGuid: assetGuid, value: auxfeeValue }, value: dustAmount })
}
}
}
valueAssetObj.outputs.forEach(output => {
assetAllocation.values.push({ n: outputs.length, value: output.value })
if (output.address === valueAssetObj.changeAddress) {
// add change index
outputs.push({ assetChangeIndex: assetAllocation.values.length - 1, type: 'BECH32', assetInfo: { assetGuid: assetGuid, value: output.value }, value: dustAmount })
} else {
outputs.push({ address: output.address, type: 'BECH32', assetInfo: { assetGuid: assetGuid, value: output.value }, value: dustAmount })
}
})
let funded = txVersion === utils.SYSCOIN_TX_VERSION_ASSET_ACTIVATE
let assetOutAccum = isAsset ? ext.BN_ZERO : utils.sumOrNaN(valueAssetObj.outputs)
const hasZeroVal = utils.hasZeroVal(valueAssetObj.outputs)
// if auxfee exists add total output for asset with auxfee so change is calculated properly
if (!ext.eq(auxfeeValue, ext.BN_ZERO)) {
assetOutAccum = ext.add(assetOutAccum, auxfeeValue)
}
// make sure if zero val is output, that zero val input is also added
const indexZeroVal = mapAssetAmounts.get(assetGuid + '-' + ext.BN_ZERO.toString(10))
if (hasZeroVal && !funded) {
if (indexZeroVal) {
inputs.push(utxos[indexZeroVal])
}
// if the required amount has filled because its 0, we've just added 0 we can exit right here
if (assetOutAccum.isZero()) {
funded = true
}
}
if (!isNonAssetFunded) {
// make sure total amount output exists
const index = mapAssetAmounts.get(assetGuid + '-' + assetOutAccum.toString(10))
// ensure every target for asset is satisfied otherwise we fail
if (!funded && index) {
inputs.push(utxos[index])
funded = true
}
assetAllocations.push(assetAllocation)
// shortcut when we know an asset spend is not funded
if (!funded) {
return utils.finalizeAssets(null, null, null, null, null)
}
} else {
assetAllocations.push(assetAllocation)
}
}
return utils.finalizeAssets(inputs, outputs, assetAllocations)
}
module.exports = {
blackjack: blackjack,
blackjackAsset: blackjackAsset
}