Make AddrBalance and TxAddresses publicly loadable from DB
parent
ab53107f47
commit
c67306ad09
242
db/rocksdb.go
242
db/rocksdb.go
|
@ -259,8 +259,8 @@ func (d *RocksDB) writeBlock(block *bchain.Block, op int) error {
|
||||||
return errors.New("DisconnectBlock is not supported for UTXO chains")
|
return errors.New("DisconnectBlock is not supported for UTXO chains")
|
||||||
}
|
}
|
||||||
addresses := make(map[string][]outpoint)
|
addresses := make(map[string][]outpoint)
|
||||||
txAddressesMap := make(map[string]*txAddresses)
|
txAddressesMap := make(map[string]*TxAddresses)
|
||||||
balances := make(map[string]*addrBalance)
|
balances := make(map[string]*AddrBalance)
|
||||||
if err := d.processAddressesUTXO(block, addresses, txAddressesMap, balances); err != nil {
|
if err := d.processAddressesUTXO(block, addresses, txAddressesMap, balances); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -296,8 +296,8 @@ type BulkConnect struct {
|
||||||
isUTXO bool
|
isUTXO bool
|
||||||
bulkAddresses []bulkAddresses
|
bulkAddresses []bulkAddresses
|
||||||
bulkAddressesCount int
|
bulkAddressesCount int
|
||||||
txAddressesMap map[string]*txAddresses
|
txAddressesMap map[string]*TxAddresses
|
||||||
balances map[string]*addrBalance
|
balances map[string]*AddrBalance
|
||||||
height uint32
|
height uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,8 +313,8 @@ func (d *RocksDB) InitBulkConnect() (*BulkConnect, error) {
|
||||||
bc := &BulkConnect{
|
bc := &BulkConnect{
|
||||||
d: d,
|
d: d,
|
||||||
isUTXO: d.chainParser.IsUTXOChain(),
|
isUTXO: d.chainParser.IsUTXOChain(),
|
||||||
txAddressesMap: make(map[string]*txAddresses),
|
txAddressesMap: make(map[string]*TxAddresses),
|
||||||
balances: make(map[string]*addrBalance),
|
balances: make(map[string]*AddrBalance),
|
||||||
}
|
}
|
||||||
if err := d.SetInconsistentState(true); err != nil {
|
if err := d.SetInconsistentState(true); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -326,18 +326,18 @@ func (d *RocksDB) InitBulkConnect() (*BulkConnect, error) {
|
||||||
func (b *BulkConnect) storeTxAddresses(c chan error, all bool) {
|
func (b *BulkConnect) storeTxAddresses(c chan error, all bool) {
|
||||||
defer close(c)
|
defer close(c)
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
var txm map[string]*txAddresses
|
var txm map[string]*TxAddresses
|
||||||
var sp int
|
var sp int
|
||||||
if all {
|
if all {
|
||||||
txm = b.txAddressesMap
|
txm = b.txAddressesMap
|
||||||
b.txAddressesMap = make(map[string]*txAddresses)
|
b.txAddressesMap = make(map[string]*TxAddresses)
|
||||||
} else {
|
} else {
|
||||||
txm = make(map[string]*txAddresses)
|
txm = make(map[string]*TxAddresses)
|
||||||
for k, a := range b.txAddressesMap {
|
for k, a := range b.txAddressesMap {
|
||||||
// store all completely spent transactions, they will not be modified again
|
// store all completely spent transactions, they will not be modified again
|
||||||
r := true
|
r := true
|
||||||
for _, o := range a.outputs {
|
for _, o := range a.Outputs {
|
||||||
if o.spent == false {
|
if o.Spent == false {
|
||||||
r = false
|
r = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -374,12 +374,12 @@ func (b *BulkConnect) storeTxAddresses(c chan error, all bool) {
|
||||||
func (b *BulkConnect) storeBalances(c chan error, all bool) {
|
func (b *BulkConnect) storeBalances(c chan error, all bool) {
|
||||||
defer close(c)
|
defer close(c)
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
var bal map[string]*addrBalance
|
var bal map[string]*AddrBalance
|
||||||
if all {
|
if all {
|
||||||
bal = b.balances
|
bal = b.balances
|
||||||
b.balances = make(map[string]*addrBalance)
|
b.balances = make(map[string]*AddrBalance)
|
||||||
} else {
|
} else {
|
||||||
bal = make(map[string]*addrBalance)
|
bal = make(map[string]*AddrBalance)
|
||||||
// store some random balances
|
// store some random balances
|
||||||
for k, a := range b.balances {
|
for k, a := range b.balances {
|
||||||
bal[k] = a
|
bal[k] = a
|
||||||
|
@ -513,26 +513,42 @@ type outpoint struct {
|
||||||
index int32
|
index int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type txInput struct {
|
type TxInput struct {
|
||||||
addrID []byte
|
addrID []byte
|
||||||
valueSat big.Int
|
ValueSat big.Int
|
||||||
}
|
}
|
||||||
|
|
||||||
type txOutput struct {
|
func (ti *TxInput) Addresses(p bchain.BlockChainParser) ([]string, error) {
|
||||||
|
// TODO - we will need AddressesFromAddrID parser method, this will not work for ZCash
|
||||||
|
return p.OutputScriptToAddresses(ti.addrID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TxOutput struct {
|
||||||
addrID []byte
|
addrID []byte
|
||||||
spent bool
|
Spent bool
|
||||||
valueSat big.Int
|
ValueSat big.Int
|
||||||
}
|
}
|
||||||
|
|
||||||
type txAddresses struct {
|
func (to *TxOutput) Addresses(p bchain.BlockChainParser) ([]string, error) {
|
||||||
inputs []txInput
|
// TODO - we will need AddressesFromAddrID parser method, this will not work for ZCash
|
||||||
outputs []txOutput
|
return p.OutputScriptToAddresses(to.addrID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type addrBalance struct {
|
type TxAddresses struct {
|
||||||
txs uint32
|
Inputs []TxInput
|
||||||
sentSat big.Int
|
Outputs []TxOutput
|
||||||
balanceSat big.Int
|
}
|
||||||
|
|
||||||
|
type AddrBalance struct {
|
||||||
|
Txs uint32
|
||||||
|
SentSat big.Int
|
||||||
|
BalanceSat big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ab *AddrBalance) ReceivedSat() big.Int {
|
||||||
|
var r big.Int
|
||||||
|
r.Add(&ab.BalanceSat, &ab.SentSat)
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
type blockTxs struct {
|
type blockTxs struct {
|
||||||
|
@ -551,7 +567,7 @@ func (d *RocksDB) resetValueSatToZero(valueSat *big.Int, addrID []byte, logText
|
||||||
valueSat.SetInt64(0)
|
valueSat.SetInt64(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RocksDB) processAddressesUTXO(block *bchain.Block, addresses map[string][]outpoint, txAddressesMap map[string]*txAddresses, balances map[string]*addrBalance) error {
|
func (d *RocksDB) processAddressesUTXO(block *bchain.Block, addresses map[string][]outpoint, txAddressesMap map[string]*TxAddresses, balances map[string]*AddrBalance) error {
|
||||||
blockTxIDs := make([][]byte, len(block.Txs))
|
blockTxIDs := make([][]byte, len(block.Txs))
|
||||||
// first process all outputs so that inputs can point to txs in this block
|
// first process all outputs so that inputs can point to txs in this block
|
||||||
for txi := range block.Txs {
|
for txi := range block.Txs {
|
||||||
|
@ -561,12 +577,12 @@ func (d *RocksDB) processAddressesUTXO(block *bchain.Block, addresses map[string
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
blockTxIDs[txi] = btxID
|
blockTxIDs[txi] = btxID
|
||||||
ta := txAddresses{}
|
ta := TxAddresses{}
|
||||||
ta.outputs = make([]txOutput, len(tx.Vout))
|
ta.Outputs = make([]TxOutput, len(tx.Vout))
|
||||||
txAddressesMap[string(btxID)] = &ta
|
txAddressesMap[string(btxID)] = &ta
|
||||||
for i, output := range tx.Vout {
|
for i, output := range tx.Vout {
|
||||||
tao := &ta.outputs[i]
|
tao := &ta.Outputs[i]
|
||||||
tao.valueSat = output.ValueSat
|
tao.ValueSat = output.ValueSat
|
||||||
addrID, err := d.chainParser.GetAddrIDFromVout(&output)
|
addrID, err := d.chainParser.GetAddrIDFromVout(&output)
|
||||||
if err != nil || len(addrID) == 0 || len(addrID) > maxAddrIDLen {
|
if err != nil || len(addrID) == 0 || len(addrID) > maxAddrIDLen {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -593,20 +609,20 @@ func (d *RocksDB) processAddressesUTXO(block *bchain.Block, addresses map[string
|
||||||
})
|
})
|
||||||
ab, e := balances[strAddrID]
|
ab, e := balances[strAddrID]
|
||||||
if !e {
|
if !e {
|
||||||
ab, err = d.getAddressBalance(addrID)
|
ab, err = d.getAddrIDBalance(addrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ab == nil {
|
if ab == nil {
|
||||||
ab = &addrBalance{}
|
ab = &AddrBalance{}
|
||||||
}
|
}
|
||||||
balances[strAddrID] = ab
|
balances[strAddrID] = ab
|
||||||
}
|
}
|
||||||
// add number of trx in balance only once, address can be multiple times in tx
|
// add number of trx in balance only once, address can be multiple times in tx
|
||||||
if !processed {
|
if !processed {
|
||||||
ab.txs++
|
ab.Txs++
|
||||||
}
|
}
|
||||||
ab.balanceSat.Add(&ab.balanceSat, &output.ValueSat)
|
ab.BalanceSat.Add(&ab.BalanceSat, &output.ValueSat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// process inputs
|
// process inputs
|
||||||
|
@ -614,9 +630,9 @@ func (d *RocksDB) processAddressesUTXO(block *bchain.Block, addresses map[string
|
||||||
tx := &block.Txs[txi]
|
tx := &block.Txs[txi]
|
||||||
spendingTxid := blockTxIDs[txi]
|
spendingTxid := blockTxIDs[txi]
|
||||||
ta := txAddressesMap[string(spendingTxid)]
|
ta := txAddressesMap[string(spendingTxid)]
|
||||||
ta.inputs = make([]txInput, len(tx.Vin))
|
ta.Inputs = make([]TxInput, len(tx.Vin))
|
||||||
for i, input := range tx.Vin {
|
for i, input := range tx.Vin {
|
||||||
tai := &ta.inputs[i]
|
tai := &ta.Inputs[i]
|
||||||
btxID, err := d.chainParser.PackTxid(input.Txid)
|
btxID, err := d.chainParser.PackTxid(input.Txid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// do not process inputs without input txid
|
// do not process inputs without input txid
|
||||||
|
@ -638,18 +654,18 @@ func (d *RocksDB) processAddressesUTXO(block *bchain.Block, addresses map[string
|
||||||
}
|
}
|
||||||
txAddressesMap[stxID] = ita
|
txAddressesMap[stxID] = ita
|
||||||
}
|
}
|
||||||
if len(ita.outputs) <= int(input.Vout) {
|
if len(ita.Outputs) <= int(input.Vout) {
|
||||||
glog.Warningf("rocksdb: height %d, tx %v, input tx %v vout %v is out of bounds of stored tx", block.Height, tx.Txid, input.Txid, input.Vout)
|
glog.Warningf("rocksdb: height %d, tx %v, input tx %v vout %v is out of bounds of stored tx", block.Height, tx.Txid, input.Txid, input.Vout)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ot := &ita.outputs[int(input.Vout)]
|
ot := &ita.Outputs[int(input.Vout)]
|
||||||
if ot.spent {
|
if ot.Spent {
|
||||||
glog.Warningf("rocksdb: height %d, tx %v, input tx %v vout %v is double spend", block.Height, tx.Txid, input.Txid, input.Vout)
|
glog.Warningf("rocksdb: height %d, tx %v, input tx %v vout %v is double spend", block.Height, tx.Txid, input.Txid, input.Vout)
|
||||||
}
|
}
|
||||||
tai.addrID = ot.addrID
|
tai.addrID = ot.addrID
|
||||||
tai.valueSat = ot.valueSat
|
tai.ValueSat = ot.ValueSat
|
||||||
// mark the output as spent in tx
|
// mark the output as spent in tx
|
||||||
ot.spent = true
|
ot.Spent = true
|
||||||
if len(ot.addrID) == 0 {
|
if len(ot.addrID) == 0 {
|
||||||
glog.Warningf("rocksdb: height %d, tx %v, input tx %v vout %v skipping empty address", block.Height, tx.Txid, input.Txid, input.Vout)
|
glog.Warningf("rocksdb: height %d, tx %v, input tx %v vout %v skipping empty address", block.Height, tx.Txid, input.Txid, input.Vout)
|
||||||
continue
|
continue
|
||||||
|
@ -667,24 +683,24 @@ func (d *RocksDB) processAddressesUTXO(block *bchain.Block, addresses map[string
|
||||||
})
|
})
|
||||||
ab, e := balances[strAddrID]
|
ab, e := balances[strAddrID]
|
||||||
if !e {
|
if !e {
|
||||||
ab, err = d.getAddressBalance(ot.addrID)
|
ab, err = d.getAddrIDBalance(ot.addrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ab == nil {
|
if ab == nil {
|
||||||
ab = &addrBalance{}
|
ab = &AddrBalance{}
|
||||||
}
|
}
|
||||||
balances[strAddrID] = ab
|
balances[strAddrID] = ab
|
||||||
}
|
}
|
||||||
// add number of trx in balance only once, address can be multiple times in tx
|
// add number of trx in balance only once, address can be multiple times in tx
|
||||||
if !processed {
|
if !processed {
|
||||||
ab.txs++
|
ab.Txs++
|
||||||
}
|
}
|
||||||
ab.balanceSat.Sub(&ab.balanceSat, &ot.valueSat)
|
ab.BalanceSat.Sub(&ab.BalanceSat, &ot.ValueSat)
|
||||||
if ab.balanceSat.Sign() < 0 {
|
if ab.BalanceSat.Sign() < 0 {
|
||||||
d.resetValueSatToZero(&ab.balanceSat, ot.addrID, "balance")
|
d.resetValueSatToZero(&ab.BalanceSat, ot.addrID, "balance")
|
||||||
}
|
}
|
||||||
ab.sentSat.Add(&ab.sentSat, &ot.valueSat)
|
ab.SentSat.Add(&ab.SentSat, &ot.ValueSat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -709,7 +725,7 @@ func (d *RocksDB) storeAddresses(wb *gorocksdb.WriteBatch, height uint32, addres
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RocksDB) storeTxAddresses(wb *gorocksdb.WriteBatch, am map[string]*txAddresses) error {
|
func (d *RocksDB) storeTxAddresses(wb *gorocksdb.WriteBatch, am map[string]*TxAddresses) error {
|
||||||
varBuf := make([]byte, maxPackedBigintBytes)
|
varBuf := make([]byte, maxPackedBigintBytes)
|
||||||
buf := make([]byte, 1024)
|
buf := make([]byte, 1024)
|
||||||
for txID, ta := range am {
|
for txID, ta := range am {
|
||||||
|
@ -719,18 +735,18 @@ func (d *RocksDB) storeTxAddresses(wb *gorocksdb.WriteBatch, am map[string]*txAd
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RocksDB) storeBalances(wb *gorocksdb.WriteBatch, abm map[string]*addrBalance) error {
|
func (d *RocksDB) storeBalances(wb *gorocksdb.WriteBatch, abm map[string]*AddrBalance) error {
|
||||||
// allocate buffer big enough for number of txs + 2 bigints
|
// allocate buffer big enough for number of txs + 2 bigints
|
||||||
buf := make([]byte, vlq.MaxLen32+2*maxPackedBigintBytes)
|
buf := make([]byte, vlq.MaxLen32+2*maxPackedBigintBytes)
|
||||||
for addrID, ab := range abm {
|
for addrID, ab := range abm {
|
||||||
// balance with 0 transactions is removed from db - happens in disconnect
|
// balance with 0 transactions is removed from db - happens in disconnect
|
||||||
if ab == nil || ab.txs <= 0 {
|
if ab == nil || ab.Txs <= 0 {
|
||||||
wb.DeleteCF(d.cfh[cfAddressBalance], []byte(addrID))
|
wb.DeleteCF(d.cfh[cfAddressBalance], []byte(addrID))
|
||||||
} else {
|
} else {
|
||||||
l := packVaruint(uint(ab.txs), buf)
|
l := packVaruint(uint(ab.Txs), buf)
|
||||||
ll := packBigint(&ab.sentSat, buf[l:])
|
ll := packBigint(&ab.SentSat, buf[l:])
|
||||||
l += ll
|
l += ll
|
||||||
ll = packBigint(&ab.balanceSat, buf[l:])
|
ll = packBigint(&ab.BalanceSat, buf[l:])
|
||||||
l += ll
|
l += ll
|
||||||
wb.PutCF(d.cfh[cfAddressBalance], []byte(addrID), buf[:l])
|
wb.PutCF(d.cfh[cfAddressBalance], []byte(addrID), buf[:l])
|
||||||
}
|
}
|
||||||
|
@ -821,7 +837,7 @@ func (d *RocksDB) getBlockTxs(height uint32) ([]blockTxs, error) {
|
||||||
return bt, nil
|
return bt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RocksDB) getAddressBalance(addrID []byte) (*addrBalance, error) {
|
func (d *RocksDB) getAddrIDBalance(addrID []byte) (*AddrBalance, error) {
|
||||||
val, err := d.db.GetCF(d.ro, d.cfh[cfAddressBalance], addrID)
|
val, err := d.db.GetCF(d.ro, d.cfh[cfAddressBalance], addrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -835,14 +851,23 @@ func (d *RocksDB) getAddressBalance(addrID []byte) (*addrBalance, error) {
|
||||||
txs, l := unpackVaruint(buf)
|
txs, l := unpackVaruint(buf)
|
||||||
sentSat, sl := unpackBigint(buf[l:])
|
sentSat, sl := unpackBigint(buf[l:])
|
||||||
balanceSat, _ := unpackBigint(buf[l+sl:])
|
balanceSat, _ := unpackBigint(buf[l+sl:])
|
||||||
return &addrBalance{
|
return &AddrBalance{
|
||||||
txs: uint32(txs),
|
Txs: uint32(txs),
|
||||||
sentSat: sentSat,
|
SentSat: sentSat,
|
||||||
balanceSat: balanceSat,
|
BalanceSat: balanceSat,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RocksDB) getTxAddresses(btxID []byte) (*txAddresses, error) {
|
// GetAddressBalance returns address balance for an address or nil if address not found
|
||||||
|
func (d *RocksDB) GetAddressBalance(address string) (*AddrBalance, error) {
|
||||||
|
addrID, err := d.chainParser.GetAddrIDFromAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d.getAddrIDBalance(addrID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RocksDB) getTxAddresses(btxID []byte) (*TxAddresses, error) {
|
||||||
val, err := d.db.GetCF(d.ro, d.cfh[cfTxAddresses], btxID)
|
val, err := d.db.GetCF(d.ro, d.cfh[cfTxAddresses], btxID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -856,79 +881,88 @@ func (d *RocksDB) getTxAddresses(btxID []byte) (*txAddresses, error) {
|
||||||
return unpackTxAddresses(buf)
|
return unpackTxAddresses(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func packTxAddresses(ta *txAddresses, buf []byte, varBuf []byte) []byte {
|
// GetTxAddresses returns TxAddresses for given txid or nil if not found
|
||||||
buf = buf[:0]
|
func (d *RocksDB) GetTxAddresses(txid string) (*TxAddresses, error) {
|
||||||
l := packVaruint(uint(len(ta.inputs)), varBuf)
|
btxID, err := d.chainParser.PackTxid(txid)
|
||||||
buf = append(buf, varBuf[:l]...)
|
if err != nil {
|
||||||
for i := range ta.inputs {
|
return nil, err
|
||||||
buf = appendTxInput(&ta.inputs[i], buf, varBuf)
|
|
||||||
}
|
}
|
||||||
l = packVaruint(uint(len(ta.outputs)), varBuf)
|
return d.getTxAddresses(btxID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func packTxAddresses(ta *TxAddresses, buf []byte, varBuf []byte) []byte {
|
||||||
|
buf = buf[:0]
|
||||||
|
l := packVaruint(uint(len(ta.Inputs)), varBuf)
|
||||||
buf = append(buf, varBuf[:l]...)
|
buf = append(buf, varBuf[:l]...)
|
||||||
for i := range ta.outputs {
|
for i := range ta.Inputs {
|
||||||
buf = appendTxOutput(&ta.outputs[i], buf, varBuf)
|
buf = appendTxInput(&ta.Inputs[i], buf, varBuf)
|
||||||
|
}
|
||||||
|
l = packVaruint(uint(len(ta.Outputs)), varBuf)
|
||||||
|
buf = append(buf, varBuf[:l]...)
|
||||||
|
for i := range ta.Outputs {
|
||||||
|
buf = appendTxOutput(&ta.Outputs[i], buf, varBuf)
|
||||||
}
|
}
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendTxInput(txi *txInput, buf []byte, varBuf []byte) []byte {
|
func appendTxInput(txi *TxInput, buf []byte, varBuf []byte) []byte {
|
||||||
la := len(txi.addrID)
|
la := len(txi.addrID)
|
||||||
l := packVaruint(uint(la), varBuf)
|
l := packVaruint(uint(la), varBuf)
|
||||||
buf = append(buf, varBuf[:l]...)
|
buf = append(buf, varBuf[:l]...)
|
||||||
buf = append(buf, txi.addrID...)
|
buf = append(buf, txi.addrID...)
|
||||||
l = packBigint(&txi.valueSat, varBuf)
|
l = packBigint(&txi.ValueSat, varBuf)
|
||||||
buf = append(buf, varBuf[:l]...)
|
buf = append(buf, varBuf[:l]...)
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendTxOutput(txo *txOutput, buf []byte, varBuf []byte) []byte {
|
func appendTxOutput(txo *TxOutput, buf []byte, varBuf []byte) []byte {
|
||||||
la := len(txo.addrID)
|
la := len(txo.addrID)
|
||||||
if txo.spent {
|
if txo.Spent {
|
||||||
la = ^la
|
la = ^la
|
||||||
}
|
}
|
||||||
l := packVarint(la, varBuf)
|
l := packVarint(la, varBuf)
|
||||||
buf = append(buf, varBuf[:l]...)
|
buf = append(buf, varBuf[:l]...)
|
||||||
buf = append(buf, txo.addrID...)
|
buf = append(buf, txo.addrID...)
|
||||||
l = packBigint(&txo.valueSat, varBuf)
|
l = packBigint(&txo.ValueSat, varBuf)
|
||||||
buf = append(buf, varBuf[:l]...)
|
buf = append(buf, varBuf[:l]...)
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func unpackTxAddresses(buf []byte) (*txAddresses, error) {
|
func unpackTxAddresses(buf []byte) (*TxAddresses, error) {
|
||||||
ta := txAddresses{}
|
ta := TxAddresses{}
|
||||||
inputs, l := unpackVaruint(buf)
|
inputs, l := unpackVaruint(buf)
|
||||||
ta.inputs = make([]txInput, inputs)
|
ta.Inputs = make([]TxInput, inputs)
|
||||||
for i := uint(0); i < inputs; i++ {
|
for i := uint(0); i < inputs; i++ {
|
||||||
l += unpackTxInput(&ta.inputs[i], buf[l:])
|
l += unpackTxInput(&ta.Inputs[i], buf[l:])
|
||||||
}
|
}
|
||||||
outputs, ll := unpackVaruint(buf[l:])
|
outputs, ll := unpackVaruint(buf[l:])
|
||||||
l += ll
|
l += ll
|
||||||
ta.outputs = make([]txOutput, outputs)
|
ta.Outputs = make([]TxOutput, outputs)
|
||||||
for i := uint(0); i < outputs; i++ {
|
for i := uint(0); i < outputs; i++ {
|
||||||
l += unpackTxOutput(&ta.outputs[i], buf[l:])
|
l += unpackTxOutput(&ta.Outputs[i], buf[l:])
|
||||||
}
|
}
|
||||||
return &ta, nil
|
return &ta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func unpackTxInput(ti *txInput, buf []byte) int {
|
func unpackTxInput(ti *TxInput, buf []byte) int {
|
||||||
al, l := unpackVaruint(buf)
|
al, l := unpackVaruint(buf)
|
||||||
ti.addrID = make([]byte, al)
|
ti.addrID = make([]byte, al)
|
||||||
copy(ti.addrID, buf[l:l+int(al)])
|
copy(ti.addrID, buf[l:l+int(al)])
|
||||||
al += uint(l)
|
al += uint(l)
|
||||||
ti.valueSat, l = unpackBigint(buf[al:])
|
ti.ValueSat, l = unpackBigint(buf[al:])
|
||||||
return l + int(al)
|
return l + int(al)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unpackTxOutput(to *txOutput, buf []byte) int {
|
func unpackTxOutput(to *TxOutput, buf []byte) int {
|
||||||
al, l := unpackVarint(buf)
|
al, l := unpackVarint(buf)
|
||||||
if al < 0 {
|
if al < 0 {
|
||||||
to.spent = true
|
to.Spent = true
|
||||||
al = ^al
|
al = ^al
|
||||||
}
|
}
|
||||||
to.addrID = make([]byte, al)
|
to.addrID = make([]byte, al)
|
||||||
copy(to.addrID, buf[l:l+al])
|
copy(to.addrID, buf[l:l+al])
|
||||||
al += l
|
al += l
|
||||||
to.valueSat, l = unpackBigint(buf[al:])
|
to.ValueSat, l = unpackBigint(buf[al:])
|
||||||
return l + al
|
return l + al
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1138,15 +1172,15 @@ func (d *RocksDB) allAddressesScan(lower uint32, higher uint32) ([][]byte, [][]b
|
||||||
return addrKeys, addrValues, nil
|
return addrKeys, addrValues, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RocksDB) disconnectTxAddresses(wb *gorocksdb.WriteBatch, height uint32, txid string, inputs []outpoint, txa *txAddresses,
|
func (d *RocksDB) disconnectTxAddresses(wb *gorocksdb.WriteBatch, height uint32, txid string, inputs []outpoint, txa *TxAddresses,
|
||||||
txAddressesToUpdate map[string]*txAddresses, balances map[string]*addrBalance) error {
|
txAddressesToUpdate map[string]*TxAddresses, balances map[string]*AddrBalance) error {
|
||||||
addresses := make(map[string]struct{})
|
addresses := make(map[string]struct{})
|
||||||
getAddressBalance := func(addrID []byte) (*addrBalance, error) {
|
getAddressBalance := func(addrID []byte) (*AddrBalance, error) {
|
||||||
var err error
|
var err error
|
||||||
s := string(addrID)
|
s := string(addrID)
|
||||||
b, fb := balances[s]
|
b, fb := balances[s]
|
||||||
if !fb {
|
if !fb {
|
||||||
b, err = d.getAddressBalance(addrID)
|
b, err = d.getAddrIDBalance(addrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1154,7 +1188,7 @@ func (d *RocksDB) disconnectTxAddresses(wb *gorocksdb.WriteBatch, height uint32,
|
||||||
}
|
}
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
for i, t := range txa.inputs {
|
for i, t := range txa.Inputs {
|
||||||
if len(t.addrID) > 0 {
|
if len(t.addrID) > 0 {
|
||||||
s := string(t.addrID)
|
s := string(t.addrID)
|
||||||
_, exist := addresses[s]
|
_, exist := addresses[s]
|
||||||
|
@ -1168,13 +1202,13 @@ func (d *RocksDB) disconnectTxAddresses(wb *gorocksdb.WriteBatch, height uint32,
|
||||||
if b != nil {
|
if b != nil {
|
||||||
// subtract number of txs only once
|
// subtract number of txs only once
|
||||||
if !exist {
|
if !exist {
|
||||||
b.txs--
|
b.Txs--
|
||||||
}
|
}
|
||||||
b.sentSat.Sub(&b.sentSat, &t.valueSat)
|
b.SentSat.Sub(&b.SentSat, &t.ValueSat)
|
||||||
if b.sentSat.Sign() < 0 {
|
if b.SentSat.Sign() < 0 {
|
||||||
d.resetValueSatToZero(&b.sentSat, t.addrID, "sent amount")
|
d.resetValueSatToZero(&b.SentSat, t.addrID, "sent amount")
|
||||||
}
|
}
|
||||||
b.balanceSat.Add(&b.balanceSat, &t.valueSat)
|
b.BalanceSat.Add(&b.BalanceSat, &t.ValueSat)
|
||||||
} else {
|
} else {
|
||||||
ad, _ := d.chainParser.OutputScriptToAddresses(t.addrID)
|
ad, _ := d.chainParser.OutputScriptToAddresses(t.addrID)
|
||||||
had := hex.EncodeToString(t.addrID)
|
had := hex.EncodeToString(t.addrID)
|
||||||
|
@ -1189,10 +1223,10 @@ func (d *RocksDB) disconnectTxAddresses(wb *gorocksdb.WriteBatch, height uint32,
|
||||||
}
|
}
|
||||||
txAddressesToUpdate[s] = sa
|
txAddressesToUpdate[s] = sa
|
||||||
}
|
}
|
||||||
sa.outputs[inputs[i].index].spent = false
|
sa.Outputs[inputs[i].index].Spent = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, t := range txa.outputs {
|
for _, t := range txa.Outputs {
|
||||||
if len(t.addrID) > 0 {
|
if len(t.addrID) > 0 {
|
||||||
s := string(t.addrID)
|
s := string(t.addrID)
|
||||||
_, exist := addresses[s]
|
_, exist := addresses[s]
|
||||||
|
@ -1206,11 +1240,11 @@ func (d *RocksDB) disconnectTxAddresses(wb *gorocksdb.WriteBatch, height uint32,
|
||||||
if b != nil {
|
if b != nil {
|
||||||
// subtract number of txs only once
|
// subtract number of txs only once
|
||||||
if !exist {
|
if !exist {
|
||||||
b.txs--
|
b.Txs--
|
||||||
}
|
}
|
||||||
b.balanceSat.Sub(&b.balanceSat, &t.valueSat)
|
b.BalanceSat.Sub(&b.BalanceSat, &t.ValueSat)
|
||||||
if b.balanceSat.Sign() < 0 {
|
if b.BalanceSat.Sign() < 0 {
|
||||||
d.resetValueSatToZero(&b.balanceSat, t.addrID, "balance")
|
d.resetValueSatToZero(&b.BalanceSat, t.addrID, "balance")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ad, _ := d.chainParser.OutputScriptToAddresses(t.addrID)
|
ad, _ := d.chainParser.OutputScriptToAddresses(t.addrID)
|
||||||
|
@ -1243,9 +1277,9 @@ func (d *RocksDB) DisconnectBlockRangeUTXO(lower uint32, higher uint32) error {
|
||||||
}
|
}
|
||||||
wb := gorocksdb.NewWriteBatch()
|
wb := gorocksdb.NewWriteBatch()
|
||||||
defer wb.Destroy()
|
defer wb.Destroy()
|
||||||
txAddressesToUpdate := make(map[string]*txAddresses)
|
txAddressesToUpdate := make(map[string]*TxAddresses)
|
||||||
txsToDelete := make(map[string]struct{})
|
txsToDelete := make(map[string]struct{})
|
||||||
balances := make(map[string]*addrBalance)
|
balances := make(map[string]*AddrBalance)
|
||||||
for height := higher; height >= lower; height-- {
|
for height := higher; height >= lower; height-- {
|
||||||
blockTxs := blocks[height-lower]
|
blockTxs := blocks[height-lower]
|
||||||
glog.Info("Disconnecting block ", height, " containing ", len(blockTxs), " transactions")
|
glog.Info("Disconnecting block ", height, " containing ", len(blockTxs), " transactions")
|
||||||
|
|
|
@ -721,6 +721,66 @@ func TestRocksDB_Index_UTXO(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
verifyAfterUTXOBlock2(t, d)
|
verifyAfterUTXOBlock2(t, d)
|
||||||
|
|
||||||
|
// test public methods for address balance and tx addresses
|
||||||
|
|
||||||
|
ab, err := d.GetAddressBalance(addr5)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
abw := &AddrBalance{
|
||||||
|
Txs: 2,
|
||||||
|
SentSat: *satB1T2A5,
|
||||||
|
BalanceSat: *satB2T3A5,
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ab, abw) {
|
||||||
|
t.Errorf("GetAddressBalance() = %+v, want %+v", ab, abw)
|
||||||
|
}
|
||||||
|
rs := ab.ReceivedSat()
|
||||||
|
rsw := satB1T2A5.Add(satB1T2A5, satB2T3A5)
|
||||||
|
if rs.Cmp(rsw) != 0 {
|
||||||
|
t.Errorf("GetAddressBalance().ReceivedSat() = %v, want %v", rs, rsw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ta, err := d.GetTxAddresses(txidB2T1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
taw := &TxAddresses{
|
||||||
|
Inputs: []TxInput{
|
||||||
|
{
|
||||||
|
addrID: addressToOutput(addr3, d.chainParser),
|
||||||
|
ValueSat: *satB1T2A3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
addrID: addressToOutput(addr2, d.chainParser),
|
||||||
|
ValueSat: *satB1T1A2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outputs: []TxOutput{
|
||||||
|
{
|
||||||
|
addrID: addressToOutput(addr6, d.chainParser),
|
||||||
|
Spent: true,
|
||||||
|
ValueSat: *satB2T1A6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
addrID: addressToOutput(addr7, d.chainParser),
|
||||||
|
Spent: false,
|
||||||
|
ValueSat: *satB2T1A7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ta, taw) {
|
||||||
|
t.Errorf("GetTxAddresses() = %+v, want %+v", ta, taw)
|
||||||
|
}
|
||||||
|
ia, err := ta.Inputs[0].Addresses(d.chainParser)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ia, []string{addr3}) {
|
||||||
|
t.Errorf("GetTxAddresses().Inputs[0].Addresses() = %v, want %v", ia, []string{addr3})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_BulkConnect_UTXO(t *testing.T) {
|
func Test_BulkConnect_UTXO(t *testing.T) {
|
||||||
|
@ -852,7 +912,7 @@ func Test_packBigint_unpackBigint(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addressToOutput(addr string, parser *btc.BitcoinParser) []byte {
|
func addressToOutput(addr string, parser bchain.BlockChainParser) []byte {
|
||||||
b, err := parser.AddressToOutputScript(addr)
|
b, err := parser.AddressToOutputScript(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -865,27 +925,27 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
hex string
|
hex string
|
||||||
data *txAddresses
|
data *TxAddresses
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "1",
|
name: "1",
|
||||||
hex: "0216001443aac20a116e09ea4f7914be1c55e4c17aa600b70016001454633aa8bd2e552bd4e89c01e73c1b7905eb58460811207cb68a199872012d001443aac20a116e09ea4f7914be1c55e4c17aa600b70101",
|
hex: "0216001443aac20a116e09ea4f7914be1c55e4c17aa600b70016001454633aa8bd2e552bd4e89c01e73c1b7905eb58460811207cb68a199872012d001443aac20a116e09ea4f7914be1c55e4c17aa600b70101",
|
||||||
data: &txAddresses{
|
data: &TxAddresses{
|
||||||
inputs: []txInput{
|
Inputs: []TxInput{
|
||||||
{
|
{
|
||||||
addrID: addressToOutput("tb1qgw4vyzs3dcy75nmezjlpc40yc9a2vq9hghdyt2", parser),
|
addrID: addressToOutput("tb1qgw4vyzs3dcy75nmezjlpc40yc9a2vq9hghdyt2", parser),
|
||||||
valueSat: *big.NewInt(0),
|
ValueSat: *big.NewInt(0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
addrID: addressToOutput("tb1q233n429a9e2jh48gnsq7w0qm0yz7kkzx0qczw8", parser),
|
addrID: addressToOutput("tb1q233n429a9e2jh48gnsq7w0qm0yz7kkzx0qczw8", parser),
|
||||||
valueSat: *big.NewInt(1234123421342341234),
|
ValueSat: *big.NewInt(1234123421342341234),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
outputs: []txOutput{
|
Outputs: []TxOutput{
|
||||||
{
|
{
|
||||||
addrID: addressToOutput("tb1qgw4vyzs3dcy75nmezjlpc40yc9a2vq9hghdyt2", parser),
|
addrID: addressToOutput("tb1qgw4vyzs3dcy75nmezjlpc40yc9a2vq9hghdyt2", parser),
|
||||||
valueSat: *big.NewInt(1),
|
ValueSat: *big.NewInt(1),
|
||||||
spent: true,
|
Spent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -893,44 +953,44 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "2",
|
name: "2",
|
||||||
hex: "0317a9149eb21980dc9d413d8eac27314938b9da920ee53e8705021918f2c017a91409f70b896169c37981d2b54b371df0d81a136a2c870501dd7e28c017a914e371782582a4addb541362c55565d2cdf56f6498870501a1e35ec0052fa9141d9ca71efa36d814424ea6ca1437e67287aebe348705012aadcac02ea91424fbc77cdc62702ade74dcf989c15e5d3f9240bc870501664894c02fa914afbfb74ee994c7d45f6698738bc4226d065266f7870501a1e35ec03276a914d2a37ce20ac9ec4f15dd05a7c6e8e9fbdb99850e88ac043b9943603376a9146b2044146a4438e6e5bfbc65f147afeb64d14fbb88ac05012a05f200",
|
hex: "0317a9149eb21980dc9d413d8eac27314938b9da920ee53e8705021918f2c017a91409f70b896169c37981d2b54b371df0d81a136a2c870501dd7e28c017a914e371782582a4addb541362c55565d2cdf56f6498870501a1e35ec0052fa9141d9ca71efa36d814424ea6ca1437e67287aebe348705012aadcac02ea91424fbc77cdc62702ade74dcf989c15e5d3f9240bc870501664894c02fa914afbfb74ee994c7d45f6698738bc4226d065266f7870501a1e35ec03276a914d2a37ce20ac9ec4f15dd05a7c6e8e9fbdb99850e88ac043b9943603376a9146b2044146a4438e6e5bfbc65f147afeb64d14fbb88ac05012a05f200",
|
||||||
data: &txAddresses{
|
data: &TxAddresses{
|
||||||
inputs: []txInput{
|
Inputs: []TxInput{
|
||||||
{
|
{
|
||||||
addrID: addressToOutput("2N7iL7AvS4LViugwsdjTB13uN4T7XhV1bCP", parser),
|
addrID: addressToOutput("2N7iL7AvS4LViugwsdjTB13uN4T7XhV1bCP", parser),
|
||||||
valueSat: *big.NewInt(9011000000),
|
ValueSat: *big.NewInt(9011000000),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
addrID: addressToOutput("2Mt9v216YiNBAzobeNEzd4FQweHrGyuRHze", parser),
|
addrID: addressToOutput("2Mt9v216YiNBAzobeNEzd4FQweHrGyuRHze", parser),
|
||||||
valueSat: *big.NewInt(8011000000),
|
ValueSat: *big.NewInt(8011000000),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
addrID: addressToOutput("2NDyqJpHvHnqNtL1F9xAeCWMAW8WLJmEMyD", parser),
|
addrID: addressToOutput("2NDyqJpHvHnqNtL1F9xAeCWMAW8WLJmEMyD", parser),
|
||||||
valueSat: *big.NewInt(7011000000),
|
ValueSat: *big.NewInt(7011000000),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
outputs: []txOutput{
|
Outputs: []TxOutput{
|
||||||
{
|
{
|
||||||
addrID: addressToOutput("2MuwoFGwABMakU7DCpdGDAKzyj2nTyRagDP", parser),
|
addrID: addressToOutput("2MuwoFGwABMakU7DCpdGDAKzyj2nTyRagDP", parser),
|
||||||
valueSat: *big.NewInt(5011000000),
|
ValueSat: *big.NewInt(5011000000),
|
||||||
spent: true,
|
Spent: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
addrID: addressToOutput("2Mvcmw7qkGXNWzkfH1EjvxDcNRGL1Kf2tEM", parser),
|
addrID: addressToOutput("2Mvcmw7qkGXNWzkfH1EjvxDcNRGL1Kf2tEM", parser),
|
||||||
valueSat: *big.NewInt(6011000000),
|
ValueSat: *big.NewInt(6011000000),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
addrID: addressToOutput("2N9GVuX3XJGHS5MCdgn97gVezc6EgvzikTB", parser),
|
addrID: addressToOutput("2N9GVuX3XJGHS5MCdgn97gVezc6EgvzikTB", parser),
|
||||||
valueSat: *big.NewInt(7011000000),
|
ValueSat: *big.NewInt(7011000000),
|
||||||
spent: true,
|
Spent: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
addrID: addressToOutput("mzii3fuRSpExMLJEHdHveW8NmiX8MPgavk", parser),
|
addrID: addressToOutput("mzii3fuRSpExMLJEHdHveW8NmiX8MPgavk", parser),
|
||||||
valueSat: *big.NewInt(999900000),
|
ValueSat: *big.NewInt(999900000),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
addrID: addressToOutput("mqHPFTRk23JZm9W1ANuEFtwTYwxjESSgKs", parser),
|
addrID: addressToOutput("mqHPFTRk23JZm9W1ANuEFtwTYwxjESSgKs", parser),
|
||||||
valueSat: *big.NewInt(5000000000),
|
ValueSat: *big.NewInt(5000000000),
|
||||||
spent: true,
|
Spent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -938,22 +998,22 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "empty address",
|
name: "empty address",
|
||||||
hex: "01000204d2020002162e010162",
|
hex: "01000204d2020002162e010162",
|
||||||
data: &txAddresses{
|
data: &TxAddresses{
|
||||||
inputs: []txInput{
|
Inputs: []TxInput{
|
||||||
{
|
{
|
||||||
addrID: []byte{},
|
addrID: []byte{},
|
||||||
valueSat: *big.NewInt(1234),
|
ValueSat: *big.NewInt(1234),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
outputs: []txOutput{
|
Outputs: []TxOutput{
|
||||||
{
|
{
|
||||||
addrID: []byte{},
|
addrID: []byte{},
|
||||||
valueSat: *big.NewInt(5678),
|
ValueSat: *big.NewInt(5678),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
addrID: []byte{},
|
addrID: []byte{},
|
||||||
valueSat: *big.NewInt(98),
|
ValueSat: *big.NewInt(98),
|
||||||
spent: true,
|
Spent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -961,9 +1021,9 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
hex: "0000",
|
hex: "0000",
|
||||||
data: &txAddresses{
|
data: &TxAddresses{
|
||||||
inputs: []txInput{},
|
Inputs: []TxInput{},
|
||||||
outputs: []txOutput{},
|
Outputs: []TxOutput{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue