forked from Atomicwallet/coinselect
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathaccumulative.js
211 lines (203 loc) · 9.6 KB
/
accumulative.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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
const utils = require('./utils')
const ext = require('./bn-extensions')
const BN = require('bn.js')
// add inputs until we reach or surpass the target value (or deplete)
// worst-case: O(n)
function accumulative (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)
let fee = ext.mul(feeRate, bytesAccum)
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)
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, feeBytes)
for (let i = 0; i < utxos.length; i++) {
const utxo = utxos[i]
const utxoBytes = utils.inputBytes(utxo)
const utxoFee = ext.mul(feeRate, utxoBytes)
const utxoValue = utils.uintOrNull(utxo.value)
// skip detrimental input
if (ext.gt(utxoFee, utxoValue)) {
if (i === utxos.length - 1) {
return { fee: ext.mul(feeRate, ext.add(bytesAccum, utxoBytes)) }
}
continue
}
bytesAccum = ext.add(bytesAccum, utxoBytes)
inAccum = ext.add(inAccum, utxoValue)
inputs.push(utxo)
// 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 (utxo.assetInfo) {
const baseAssetID = utils.getBaseAssetID(utxo.assetInfo.assetGuid)
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)
// 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)
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 === utxo.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)
}
}
}
fee = ext.mul(feeRate, bytesAccum)
// go again?
if (ext.lt(inAccum, ext.add(outAccum, fee))) continue
return utils.finalize(inputs, outputs, feeRate, feeBytes)
}
return { fee: ext.mul(feeRate, bytesAccum) }
}
// worst-case: O(n)
function accumulativeAsset (utxoAssets, 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 assetAllocations = []
const outputs = []
const inputs = []
let auxfeeValue = ext.BN_ZERO
// 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 and its not an NFT
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 })
}
})
const hasZeroVal = utils.hasZeroVal(valueAssetObj.outputs)
let assetOutAccum = isAsset ? ext.BN_ZERO : utils.sumOrNaN(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)
}
// order by descending asset amounts for this asset guid
let utxoAsset = utxoAssets.filter(utxo => utxo.assetInfo.assetGuid === assetGuid)
utxoAsset = utxoAsset.concat().sort(function (a, b) {
return ext.sub(b.assetInfo.value, a.assetInfo.value)
})
let funded = txVersion === utils.SYSCOIN_TX_VERSION_ASSET_ACTIVATE
// look for zero val input if zero val output exists
if (hasZeroVal && !funded) {
let foundZeroVal = false
for (let i = utxoAsset.length - 1; i >= 0; i--) {
const utxo = utxoAsset[i]
const utxoValue = utils.uintOrNull(utxo.assetInfo.value)
if (!utxoValue.isZero()) {
continue
}
inputs.push(utxo)
foundZeroVal = true
// if requested output was 0 then we should be done
if (assetOutAccum.isZero()) {
funded = true
}
break
}
if (!foundZeroVal) {
return utils.finalizeAssets(null, null, null, null, null)
}
}
if (!funded && !isNonAssetFunded) {
// order by descending asset amounts for this asset guid
let utxoAsset = utxoAssets.filter(utxo => utxo.assetInfo.assetGuid === assetGuid)
utxoAsset = utxoAsset.concat().sort(function (a, b) {
return ext.sub(b.assetInfo.value, a.assetInfo.value)
})
let inAccum = ext.BN_ZERO
for (let i = 0; i < utxoAsset.length; i++) {
const utxo = utxoAsset[i]
const utxoValue = utils.uintOrNull(utxo.assetInfo.value)
// if not funding asset new/update/send, we should fund with non-zero asset utxo amounts only
if (!hasZeroVal && utxoValue.isZero()) {
continue
}
inAccum = ext.add(inAccum, utxoValue)
inputs.push(utxo)
// deal with change
if (ext.gt(inAccum, assetOutAccum)) {
const changeAsset = ext.sub(inAccum, assetOutAccum)
// add output as dust amount (smallest possible sys output)
const output = { assetChangeIndex: assetAllocation.values.length, type: 'BECH32', assetInfo: { assetGuid: assetGuid, value: changeAsset }, value: dustAmount }
// but asset commitment will have the full asset change value
assetAllocation.values.push({ n: outputs.length, value: changeAsset })
outputs.push(output)
break
// no change, in = out
} else if (ext.eq(inAccum, assetOutAccum)) {
break
}
}
}
assetAllocations.push(assetAllocation)
}
return utils.finalizeAssets(inputs, outputs, assetAllocations)
}
module.exports = {
accumulative: accumulative,
accumulativeAsset: accumulativeAsset
}