[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

@@ -35,14 +35,6 @@ func TestVtxosToTxs(t *testing.T) {
name: "Alice After Sending Async", name: "Alice After Sending Async",
fixture: aliceAfterSendingAsync, fixture: aliceAfterSendingAsync,
want: []Transaction{ want: []Transaction{
{
RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad",
Amount: 1000,
Type: TxSent,
Pending: true,
Claimed: false,
CreatedAt: time.Unix(1726054898, 0),
},
{ {
RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf",
Amount: 20000, Amount: 20000,
@@ -51,6 +43,14 @@ func TestVtxosToTxs(t *testing.T) {
Claimed: true, Claimed: true,
CreatedAt: time.Unix(1726054898, 0), CreatedAt: time.Unix(1726054898, 0),
}, },
{
RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad",
Amount: 1000,
Type: TxSent,
Pending: true,
Claimed: false,
CreatedAt: time.Unix(1726054898, 0),
},
}, },
}, },
{ {
@@ -131,11 +131,11 @@ func TestVtxosToTxs(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
args, err := loadFixtures(tt.fixture) vtxos, boardingTxs, err := loadFixtures(tt.fixture)
if err != nil { if err != nil {
t.Fatalf("failed to load fixture: %s", err) t.Fatalf("failed to load fixture: %s", err)
} }
got, err := vtxosToTxsCovenantless(30, args.spendable, args.spent) got, err := vtxosToTxsCovenantless(30, vtxos.spendable, vtxos.spent, boardingTxs)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, got, len(tt.want)) require.Len(t, got, len(tt.want))
@@ -158,8 +158,17 @@ type vtxos struct {
spent []client.Vtxo spent []client.Vtxo
} }
func loadFixtures(jsonStr string) (vtxos, error) { func loadFixtures(jsonStr string) (vtxos, []Transaction, error) {
var data struct { var data struct {
BoardingTxs []struct {
BoardingTxid string `json:"boardingTxid"`
RoundTxid string `json:"roundTxid"`
Amount uint64 `json:"amount"`
Type TxType `json:"txType"`
Pending bool `json:"pending"`
Claimed bool `json:"claimed"`
CreatedAt string `json:"createdAt"`
} `json:"boardingTxs"`
SpendableVtxos []struct { SpendableVtxos []struct {
Outpoint struct { Outpoint struct {
Txid string `json:"txid"` Txid string `json:"txid"`
@@ -203,18 +212,18 @@ func loadFixtures(jsonStr string) (vtxos, error) {
} }
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return vtxos{}, err return vtxos{}, nil, err
} }
spendable := make([]client.Vtxo, len(data.SpendableVtxos)) spendable := make([]client.Vtxo, len(data.SpendableVtxos))
for i, vtxo := range data.SpendableVtxos { for i, vtxo := range data.SpendableVtxos {
expireAt, err := parseTimestamp(vtxo.ExpireAt) expireAt, err := parseTimestamp(vtxo.ExpireAt)
if err != nil { if err != nil {
return vtxos{}, err return vtxos{}, nil, err
} }
amount, err := parseAmount(vtxo.Receiver.Amount) amount, err := parseAmount(vtxo.Receiver.Amount)
if err != nil { if err != nil {
return vtxos{}, err return vtxos{}, nil, err
} }
spendable[i] = client.Vtxo{ spendable[i] = client.Vtxo{
VtxoKey: client.VtxoKey{ VtxoKey: client.VtxoKey{
@@ -235,11 +244,11 @@ func loadFixtures(jsonStr string) (vtxos, error) {
for i, vtxo := range data.SpentVtxos { for i, vtxo := range data.SpentVtxos {
expireAt, err := parseTimestamp(vtxo.ExpireAt) expireAt, err := parseTimestamp(vtxo.ExpireAt)
if err != nil { if err != nil {
return vtxos{}, err return vtxos{}, nil, err
} }
amount, err := parseAmount(vtxo.Receiver.Amount) amount, err := parseAmount(vtxo.Receiver.Amount)
if err != nil { if err != nil {
return vtxos{}, err return vtxos{}, nil, err
} }
spent[i] = client.Vtxo{ spent[i] = client.Vtxo{
VtxoKey: client.VtxoKey{ VtxoKey: client.VtxoKey{
@@ -256,10 +265,29 @@ func loadFixtures(jsonStr string) (vtxos, error) {
} }
} }
return vtxos{ boardingTxs := make([]Transaction, len(data.BoardingTxs))
for i, tx := range data.BoardingTxs {
createdAt, err := parseTimestamp(tx.CreatedAt)
if err != nil {
return vtxos{}, nil, err
}
boardingTxs[i] = Transaction{
BoardingTxid: tx.BoardingTxid,
RoundTxid: tx.RoundTxid,
Amount: tx.Amount,
Type: TxReceived,
Pending: tx.Pending,
Claimed: tx.Claimed,
CreatedAt: createdAt,
}
}
vtxos := vtxos{
spendable: spendable, spendable: spendable,
spent: spent, spent: spent,
}, nil }
return vtxos, boardingTxs, nil
} }
func parseAmount(amountStr string) (uint64, error) { func parseAmount(amountStr string) (uint64, error) {
@@ -287,6 +315,16 @@ func parseTimestamp(timestamp string) (time.Time, error) {
var ( var (
aliceBeforeSendingAsync = ` aliceBeforeSendingAsync = `
{ {
"boardingTxs": [
{
"boardingTxid": "69ccb6520e0b91ac1cbaa459b16ec1e3ff5f6349990b0d149dd8e6c6485d316c",
"roundTxid": "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf",
"amount": 20000,
"pending": false,
"claimed": true,
"createdAt": "1726503865"
}
],
"spendableVtxos": [ "spendableVtxos": [
{ {
"outpoint": { "outpoint": {
@@ -311,6 +349,16 @@ var (
aliceAfterSendingAsync = ` aliceAfterSendingAsync = `
{ {
"boardingTxs": [
{
"boardingTxid": "69ccb6520e0b91ac1cbaa459b16ec1e3ff5f6349990b0d149dd8e6c6485d316c",
"roundTxid": "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf",
"amount": 20000,
"pending": false,
"claimed": true,
"createdAt": "1726503865"
}
],
"spendableVtxos": [ "spendableVtxos": [
{ {
"outpoint": { "outpoint": {

View File

@@ -521,88 +521,42 @@ func (a *covenantArkClient) GetTransactionHistory(ctx context.Context) ([]Transa
return nil, err return nil, err
} }
return vtxosToTxsCovenant(config.RoundLifetime, spendableVtxos, spentVtxos) boardingTxs := a.getBoardingTxs(ctx)
return vtxosToTxsCovenant(config.RoundLifetime, spendableVtxos, spentVtxos, boardingTxs)
} }
func vtxosToTxsCovenant(roundLifetime int64, spendable, spent []client.Vtxo) ([]Transaction, error) { func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
transactions := make([]Transaction, 0) _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, err
}
for _, v := range append(spendable, spent...) { utxos := []explorer.Utxo{}
// get vtxo amount for _, addr := range boardingAddrs {
amount := int(v.Amount) txs, err := a.explorer.GetTxs(addr)
if v.Pending { if err != nil {
// find other spent vtxos that spent this one continue
relatedVtxos := findVtxosBySpentBy(spent, v.Txid) }
for _, r := range relatedVtxos { for _, tx := range txs {
if r.Amount < math.MaxInt64 { for i, vout := range tx.Vout {
rAmount := int(r.Amount) if vout.Address == addr {
amount -= rAmount 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,
})
} }
} }
} 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 := getRedeemTxidCovenant(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 return utxos, nil
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 getRedeemTxidCovenant(redeemTx string) (string, error) {
redeemPtx, err := psetv2.NewPsetFromBase64(redeemTx)
if err != nil {
return "", fmt.Errorf("failed to parse redeem tx: %s", err)
}
tx, err := redeemPtx.UnsignedTx()
if err != nil {
return "", fmt.Errorf("failed to get txid from redeem tx: %s", err)
}
return tx.TxHash().String(), nil
} }
func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) { func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
@@ -1467,3 +1421,124 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
return roundTxid, nil return roundTxid, nil
} }
func (a *covenantArkClient) 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 vtxosToTxsCovenant(
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)
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 := getRedeemTxidCovenant(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 getRedeemTxidCovenant(redeemTx string) (string, error) {
redeemPtx, err := psetv2.NewPsetFromBase64(redeemTx)
if err != nil {
return "", fmt.Errorf("failed to parse redeem tx: %s", err)
}
tx, err := redeemPtx.UnsignedTx()
if err != nil {
return "", fmt.Errorf("failed to get txid from redeem tx: %s", err)
}
return tx.TxHash().String(), nil
}

View File

@@ -632,83 +632,9 @@ func (a *covenantlessArkClient) GetTransactionHistory(ctx context.Context) ([]Tr
return nil, err return nil, err
} }
return vtxosToTxsCovenantless(config.RoundLifetime, spendableVtxos, spentVtxos) boardingTxs := a.getBoardingTxs(ctx)
}
func vtxosToTxsCovenantless(roundLifetime int64, spendable, spent []client.Vtxo) ([]Transaction, error) { return vtxosToTxsCovenantless(config.RoundLifetime, spendableVtxos, spentVtxos, boardingTxs)
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
} }
func (a *covenantlessArkClient) sendOnchain( func (a *covenantlessArkClient) sendOnchain(
@@ -1571,6 +1497,39 @@ func (a *covenantlessArkClient) getOffchainBalance(
return balance, amountByExpiration, nil 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) { func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil { if err != nil {
@@ -1710,6 +1669,39 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
return roundTxid, nil 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) { func findVtxosBySpentBy(allVtxos []client.Vtxo, txid string) (vtxos []client.Vtxo) {
for _, v := range allVtxos { for _, v := range allVtxos {
if v.SpentBy == txid { if v.SpentBy == txid {
@@ -1718,3 +1710,87 @@ func findVtxosBySpentBy(allVtxos []client.Vtxo, txid string) (vtxos []client.Vtx
} }
return 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
}

View File

@@ -31,6 +31,7 @@ type Utxo struct {
Asset string // liquid only Asset string // liquid only
Delay uint Delay uint
SpendableAt time.Time SpendableAt time.Time
CreatedAt time.Time
} }
func (u *Utxo) Sequence() (uint32, error) { func (u *Utxo) Sequence() (uint32, error) {
@@ -39,7 +40,9 @@ func (u *Utxo) Sequence() (uint32, error) {
func newUtxo(explorerUtxo ExplorerUtxo, delay uint) Utxo { func newUtxo(explorerUtxo ExplorerUtxo, delay uint) Utxo {
utxoTime := explorerUtxo.Status.Blocktime utxoTime := explorerUtxo.Status.Blocktime
createdAt := time.Unix(utxoTime, 0)
if utxoTime == 0 { if utxoTime == 0 {
createdAt = time.Time{}
utxoTime = time.Now().Unix() utxoTime = time.Now().Unix()
} }
@@ -50,9 +53,22 @@ func newUtxo(explorerUtxo ExplorerUtxo, delay uint) Utxo {
Asset: explorerUtxo.Asset, Asset: explorerUtxo.Asset,
Delay: delay, Delay: delay,
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second), SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second),
CreatedAt: createdAt,
} }
} }
type ExplorerTx struct {
Txid string `json:"txid"`
Vout []struct {
Address string `json:"scriptpubkey_address"`
Amount uint64 `json:"value"`
} `json:"vout"`
Status struct {
Confirmed bool `json:"confirmed"`
Blocktime int64 `json:"block_time"`
} `json:"status"`
}
type ExplorerUtxo struct { type ExplorerUtxo struct {
Txid string `json:"txid"` Txid string `json:"txid"`
Vout uint32 `json:"vout"` Vout uint32 `json:"vout"`
@@ -71,6 +87,7 @@ func (e ExplorerUtxo) ToUtxo(delay uint) Utxo {
type Explorer interface { type Explorer interface {
GetTxHex(txid string) (string, error) GetTxHex(txid string) (string, error)
Broadcast(txHex string) (string, error) Broadcast(txHex string) (string, error)
GetTxs(addr string) ([]ExplorerTx, error)
GetUtxos(addr string) ([]ExplorerUtxo, error) GetUtxos(addr string) ([]ExplorerUtxo, error)
GetBalance(addr string) (uint64, error) GetBalance(addr string) (uint64, error)
GetRedeemedVtxosBalance( GetRedeemedVtxosBalance(
@@ -176,6 +193,28 @@ func (e *explorerSvc) Broadcast(txStr string) (string, error) {
return txid, nil return txid, nil
} }
func (e *explorerSvc) GetTxs(addr string) ([]ExplorerTx, error) {
resp, err := http.Get(fmt.Sprintf("%s/address/%s/txs", e.baseUrl, addr))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get txs: %s", string(body))
}
payload := []ExplorerTx{}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
return payload, nil
}
func (e *explorerSvc) GetUtxos(addr string) ([]ExplorerUtxo, error) { func (e *explorerSvc) GetUtxos(addr string) ([]ExplorerUtxo, error) {
resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", e.baseUrl, addr)) resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", e.baseUrl, addr))
if err != nil { if err != nil {

View File

@@ -131,11 +131,12 @@ const (
type TxType string type TxType string
type Transaction struct { type Transaction struct {
RoundTxid string BoardingTxid string
RedeemTxid string RoundTxid string
Amount uint64 RedeemTxid string
Type TxType Amount uint64
Pending bool Type TxType
Claimed bool Pending bool
CreatedAt time.Time Claimed bool
CreatedAt time.Time
} }