mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-18 04:34:19 +01:00
[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:
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user