[SDK] Add boarding txs to history (#306)

* test

* Add onboard tx to tx history

Co-authored-by: Pietralberto Mazza <altafan@users.noreply.github.com>

* add createdAt to onboard UTXO

* show unconfirmed on top

* add new method GetTx to explorer

* fix list of onboarding tx

* ignore not pending

* small refactor

* replicate changes on covenant client

* fix tests

---------

Co-authored-by: Pietralberto Mazza <altafan@users.noreply.github.com>
This commit is contained in:
João Bordalo
2024-09-13 18:19:21 +01:00
committed by GitHub
parent 4304626d08
commit 2174e4b04d
5 changed files with 414 additions and 175 deletions

View File

@@ -632,83 +632,9 @@ func (a *covenantlessArkClient) GetTransactionHistory(ctx context.Context) ([]Tr
return nil, err
}
return vtxosToTxsCovenantless(config.RoundLifetime, spendableVtxos, spentVtxos)
}
boardingTxs := a.getBoardingTxs(ctx)
func vtxosToTxsCovenantless(roundLifetime int64, spendable, spent []client.Vtxo) ([]Transaction, error) {
transactions := make([]Transaction, 0)
for _, v := range append(spendable, spent...) {
// get vtxo amount
amount := int(v.Amount)
if v.Pending {
// find other spent vtxos that spent this one
relatedVtxos := findVtxosBySpentBy(spent, v.Txid)
for _, r := range relatedVtxos {
if r.Amount < math.MaxInt64 {
rAmount := int(r.Amount)
amount -= rAmount
}
}
} else {
// an onboarding tx has pending false and no pending true related txs
relatedVtxos := findVtxosBySpentBy(spent, v.RoundTxid)
if len(relatedVtxos) > 0 { // not an onboard tx, ignore
continue
}
} // what kind of tx was this? send or receive?
txType := TxReceived
if amount < 0 {
txType = TxSent
}
// check if is a pending tx
pending := false
claimed := true
if len(v.RoundTxid) == 0 && len(v.SpentBy) == 0 {
pending = true
claimed = false
}
redeemTxid := ""
if len(v.RedeemTx) > 0 {
txid, err := getRedeemTxidCovenantless(v.RedeemTx)
if err != nil {
return nil, err
}
redeemTxid = txid
}
// add transaction
transactions = append(transactions, Transaction{
RoundTxid: v.RoundTxid,
RedeemTxid: redeemTxid,
Amount: uint64(math.Abs(float64(amount))),
Type: txType,
Pending: pending,
Claimed: claimed,
CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt),
})
}
// Sort the slice by age
sort.Slice(transactions, func(i, j int) bool {
txi := transactions[i]
txj := transactions[j]
if txi.CreatedAt.Equal(txj.CreatedAt) {
return txi.Type > txj.Type
}
return txi.CreatedAt.After(txj.CreatedAt)
})
return transactions, nil
}
func getRedeemTxidCovenantless(redeemTx string) (string, error) {
redeemPtx, err := psbt.NewFromRawBytes(strings.NewReader(redeemTx), true)
if err != nil {
return "", fmt.Errorf("failed to parse redeem tx: %s", err)
}
return redeemPtx.UnsignedTx.TxID(), nil
return vtxosToTxsCovenantless(config.RoundLifetime, spendableVtxos, spentVtxos, boardingTxs)
}
func (a *covenantlessArkClient) sendOnchain(
@@ -1571,6 +1497,39 @@ func (a *covenantlessArkClient) getOffchainBalance(
return balance, amountByExpiration, nil
}
func (a *covenantlessArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
_, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, err
}
utxos := []explorer.Utxo{}
for _, addr := range boardingAddrs {
txs, err := a.explorer.GetTxs(addr)
if err != nil {
continue
}
for _, tx := range txs {
for i, vout := range tx.Vout {
if vout.Address == addr {
createdAt := time.Time{}
if tx.Status.Confirmed {
createdAt = time.Unix(tx.Status.Blocktime, 0)
}
utxos = append(utxos, explorer.Utxo{
Txid: tx.Txid,
Vout: uint32(i),
Amount: vout.Amount,
CreatedAt: createdAt,
})
}
}
}
}
return utxos, nil
}
func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil {
@@ -1710,6 +1669,39 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
return roundTxid, nil
}
func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) {
utxos, err := a.getClaimableBoardingUtxos(ctx)
if err != nil {
return nil
}
isPending := make(map[string]bool)
for _, u := range utxos {
isPending[u.Txid] = true
}
allUtxos, err := a.getAllBoardingUtxos(ctx)
if err != nil {
return nil
}
for _, u := range allUtxos {
pending := false
if isPending[u.Txid] {
pending = true
}
transactions = append(transactions, Transaction{
BoardingTxid: u.Txid,
Amount: u.Amount,
Type: TxReceived,
Pending: pending,
Claimed: !pending,
CreatedAt: u.CreatedAt,
})
}
return
}
func findVtxosBySpentBy(allVtxos []client.Vtxo, txid string) (vtxos []client.Vtxo) {
for _, v := range allVtxos {
if v.SpentBy == txid {
@@ -1718,3 +1710,87 @@ func findVtxosBySpentBy(allVtxos []client.Vtxo, txid string) (vtxos []client.Vtx
}
return
}
func vtxosToTxsCovenantless(
roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []Transaction,
) ([]Transaction, error) {
transactions := make([]Transaction, 0)
unconfirmedBoardingTxs := make([]Transaction, 0)
for _, tx := range boardingTxs {
emptyTime := time.Time{}
if tx.CreatedAt == emptyTime {
unconfirmedBoardingTxs = append(unconfirmedBoardingTxs, tx)
continue
}
transactions = append(transactions, tx)
}
for _, v := range append(spendable, spent...) {
// get vtxo amount
amount := int(v.Amount)
// ignore not pending
if !v.Pending {
continue
}
// find other spent vtxos that spent this one
relatedVtxos := findVtxosBySpentBy(spent, v.Txid)
for _, r := range relatedVtxos {
if r.Amount < math.MaxInt64 {
rAmount := int(r.Amount)
amount -= rAmount
}
}
// what kind of tx was this? send or receive?
txType := TxReceived
if amount < 0 {
txType = TxSent
}
// check if is a pending tx
pending := false
claimed := true
if len(v.RoundTxid) == 0 && len(v.SpentBy) == 0 {
pending = true
claimed = false
}
// get redeem txid
redeemTxid := ""
if len(v.RedeemTx) > 0 {
txid, err := getRedeemTxidCovenantless(v.RedeemTx)
if err != nil {
return nil, err
}
redeemTxid = txid
}
// add transaction
transactions = append(transactions, Transaction{
RoundTxid: v.RoundTxid,
RedeemTxid: redeemTxid,
Amount: uint64(math.Abs(float64(amount))),
Type: txType,
Pending: pending,
Claimed: claimed,
CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt),
})
}
// Sort the slice by age
sort.Slice(transactions, func(i, j int) bool {
txi := transactions[i]
txj := transactions[j]
if txi.CreatedAt.Equal(txj.CreatedAt) {
return txi.Type > txj.Type
}
return txi.CreatedAt.After(txj.CreatedAt)
})
return append(unconfirmedBoardingTxs, transactions...), nil
}
func getRedeemTxidCovenantless(redeemTx string) (string, error) {
redeemPtx, err := psbt.NewFromRawBytes(strings.NewReader(redeemTx), true)
if err != nil {
return "", fmt.Errorf("failed to parse redeem tx: %s", err)
}
return redeemPtx.UnsignedTx.TxID(), nil
}