mirror of
https://github.com/aljazceru/ark.git
synced 2026-01-22 05:04:20 +01:00
Drop PendingChange field (#331)
* Drop pending_change * Fixes * Polish * Fallback to psbt string
This commit is contained in:
committed by
GitHub
parent
2be78b0115
commit
b15c0868b2
@@ -88,7 +88,6 @@ type Vtxo struct {
|
||||
RedeemTx string
|
||||
UnconditionalForfeitTxs []string
|
||||
Pending bool
|
||||
PendingChange bool
|
||||
SpentBy string
|
||||
}
|
||||
|
||||
|
||||
@@ -462,7 +462,6 @@ func (v vtxo) toVtxo() client.Vtxo {
|
||||
RoundTxid: v.GetPoolTxid(),
|
||||
ExpiresAt: expiresAt,
|
||||
Pending: v.GetPending(),
|
||||
PendingChange: v.GetPendingChange(),
|
||||
RedeemTx: redeemTx,
|
||||
UnconditionalForfeitTxs: uncondForfeitTxs,
|
||||
SpentBy: v.GetSpentBy(),
|
||||
|
||||
@@ -177,7 +177,6 @@ func (a *restClient) ListVtxos(
|
||||
RoundTxid: v.PoolTxid,
|
||||
ExpiresAt: expiresAt,
|
||||
Pending: v.Pending,
|
||||
PendingChange: v.PendingChange,
|
||||
RedeemTx: redeemTx,
|
||||
UnconditionalForfeitTxs: uncondForfeitTxs,
|
||||
SpentBy: v.SpentBy,
|
||||
|
||||
@@ -33,9 +33,6 @@ type V1Vtxo struct {
|
||||
// pending
|
||||
Pending bool `json:"pending,omitempty"`
|
||||
|
||||
// pending change
|
||||
PendingChange bool `json:"pendingChange,omitempty"`
|
||||
|
||||
// pending data
|
||||
PendingData *V1PendingPayment `json:"pendingData,omitempty"`
|
||||
|
||||
|
||||
@@ -20,32 +20,17 @@ func TestVtxosToTxs(t *testing.T) {
|
||||
{
|
||||
name: "Alice Before Sending Async",
|
||||
fixture: aliceBeforeSendingAsync,
|
||||
want: []Transaction{
|
||||
{
|
||||
RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf",
|
||||
Amount: 20000,
|
||||
Type: TxReceived,
|
||||
IsPending: false,
|
||||
CreatedAt: time.Unix(1726054898, 0),
|
||||
},
|
||||
},
|
||||
want: []Transaction{},
|
||||
},
|
||||
{
|
||||
name: "Alice After Sending Async",
|
||||
fixture: aliceAfterSendingAsync,
|
||||
want: []Transaction{
|
||||
{
|
||||
RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf",
|
||||
Amount: 20000,
|
||||
Type: TxReceived,
|
||||
IsPending: false,
|
||||
CreatedAt: time.Unix(1726054898, 0),
|
||||
},
|
||||
{
|
||||
RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad",
|
||||
Amount: 1000,
|
||||
Type: TxSent,
|
||||
IsPending: true,
|
||||
IsPending: false,
|
||||
CreatedAt: time.Unix(1726054898, 0),
|
||||
},
|
||||
},
|
||||
@@ -54,13 +39,6 @@ func TestVtxosToTxs(t *testing.T) {
|
||||
name: "Bob Before Claiming Async",
|
||||
fixture: bobBeforeClaimingAsync,
|
||||
want: []Transaction{
|
||||
{
|
||||
RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367",
|
||||
Amount: 2000,
|
||||
Type: TxReceived,
|
||||
IsPending: true,
|
||||
CreatedAt: time.Unix(1726486359, 0),
|
||||
},
|
||||
{
|
||||
RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad",
|
||||
Amount: 1000,
|
||||
@@ -68,19 +46,19 @@ func TestVtxosToTxs(t *testing.T) {
|
||||
IsPending: true,
|
||||
CreatedAt: time.Unix(1726054898, 0),
|
||||
},
|
||||
{
|
||||
RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367",
|
||||
Amount: 2000,
|
||||
Type: TxReceived,
|
||||
IsPending: true,
|
||||
CreatedAt: time.Unix(1726486359, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Bob After Claiming Async",
|
||||
fixture: bobAfterClaimingAsync,
|
||||
want: []Transaction{
|
||||
{
|
||||
RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367",
|
||||
Amount: 2000,
|
||||
Type: TxReceived,
|
||||
IsPending: false,
|
||||
CreatedAt: time.Unix(1726486359, 0),
|
||||
},
|
||||
{
|
||||
RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad",
|
||||
Amount: 1000,
|
||||
@@ -88,6 +66,13 @@ func TestVtxosToTxs(t *testing.T) {
|
||||
IsPending: false,
|
||||
CreatedAt: time.Unix(1726054898, 0),
|
||||
},
|
||||
{
|
||||
RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367",
|
||||
Amount: 2000,
|
||||
Type: TxReceived,
|
||||
IsPending: false,
|
||||
CreatedAt: time.Unix(1726486359, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -95,11 +80,11 @@ func TestVtxosToTxs(t *testing.T) {
|
||||
fixture: bobAfterSendingAsync,
|
||||
want: []Transaction{
|
||||
{
|
||||
RedeemTxid: "23c3a885f0ea05f7bdf83f3bf7f8ac9dc3f791ad292f4e63a6f53fa5e4935ab0",
|
||||
Amount: 2100,
|
||||
Type: TxSent,
|
||||
IsPending: true,
|
||||
CreatedAt: time.Unix(1726503865, 0),
|
||||
RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad",
|
||||
Amount: 1000,
|
||||
Type: TxReceived,
|
||||
IsPending: false,
|
||||
CreatedAt: time.Unix(1726054898, 0),
|
||||
},
|
||||
{
|
||||
RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367",
|
||||
@@ -109,11 +94,11 @@ func TestVtxosToTxs(t *testing.T) {
|
||||
CreatedAt: time.Unix(1726486359, 0),
|
||||
},
|
||||
{
|
||||
RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad",
|
||||
Amount: 1000,
|
||||
Type: TxReceived,
|
||||
RedeemTxid: "23c3a885f0ea05f7bdf83f3bf7f8ac9dc3f791ad292f4e63a6f53fa5e4935ab0",
|
||||
Amount: 2100,
|
||||
Type: TxSent,
|
||||
IsPending: false,
|
||||
CreatedAt: time.Unix(1726054898, 0),
|
||||
CreatedAt: time.Unix(1726503865, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -121,11 +106,11 @@ func TestVtxosToTxs(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
vtxos, boardingTxs, err := loadFixtures(tt.fixture)
|
||||
vtxos, ignoreTxs, err := loadFixtures(tt.fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load fixture: %s", err)
|
||||
}
|
||||
got, err := vtxosToTxsCovenantless(30, vtxos.spendable, vtxos.spent, boardingTxs)
|
||||
got, err := vtxosToTxsCovenantless(30, vtxos.spendable, vtxos.spent, ignoreTxs)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, len(tt.want))
|
||||
|
||||
@@ -147,17 +132,9 @@ type vtxos struct {
|
||||
spent []client.Vtxo
|
||||
}
|
||||
|
||||
func loadFixtures(jsonStr string) (vtxos, []Transaction, error) {
|
||||
func loadFixtures(jsonStr string) (vtxos, map[string]struct{}, error) {
|
||||
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"`
|
||||
IgnoreTxs []string `json:"ignoreTxs"`
|
||||
SpendableVtxos []struct {
|
||||
Outpoint struct {
|
||||
Txid string `json:"txid"`
|
||||
@@ -254,28 +231,17 @@ func loadFixtures(jsonStr string) (vtxos, []Transaction, error) {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
IsPending: tx.Pending,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
vtxos := vtxos{
|
||||
spendable: spendable,
|
||||
spent: spent,
|
||||
}
|
||||
|
||||
return vtxos, boardingTxs, nil
|
||||
ignoreTxs := make(map[string]struct{})
|
||||
for _, tx := range data.IgnoreTxs {
|
||||
ignoreTxs[tx] = struct{}{}
|
||||
}
|
||||
|
||||
return vtxos, ignoreTxs, nil
|
||||
}
|
||||
|
||||
func parseAmount(amountStr string) (uint64, error) {
|
||||
@@ -303,15 +269,8 @@ func parseTimestamp(timestamp string) (time.Time, error) {
|
||||
var (
|
||||
aliceBeforeSendingAsync = `
|
||||
{
|
||||
"boardingTxs": [
|
||||
{
|
||||
"boardingTxid": "69ccb6520e0b91ac1cbaa459b16ec1e3ff5f6349990b0d149dd8e6c6485d316c",
|
||||
"roundTxid": "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf",
|
||||
"amount": 20000,
|
||||
"pending": false,
|
||||
"claimed": true,
|
||||
"createdAt": "1726503865"
|
||||
}
|
||||
"ignoreTxs": [
|
||||
"377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf"
|
||||
],
|
||||
"spendableVtxos": [
|
||||
{
|
||||
@@ -337,15 +296,8 @@ var (
|
||||
|
||||
aliceAfterSendingAsync = `
|
||||
{
|
||||
"boardingTxs": [
|
||||
{
|
||||
"boardingTxid": "69ccb6520e0b91ac1cbaa459b16ec1e3ff5f6349990b0d149dd8e6c6485d316c",
|
||||
"roundTxid": "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf",
|
||||
"amount": 20000,
|
||||
"pending": false,
|
||||
"claimed": true,
|
||||
"createdAt": "1726503865"
|
||||
}
|
||||
"ignoreTxs": [
|
||||
"377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf"
|
||||
],
|
||||
"spendableVtxos": [
|
||||
{
|
||||
@@ -362,7 +314,7 @@ var (
|
||||
"spentBy": "",
|
||||
"expireAt": "1726054928",
|
||||
"swept": false,
|
||||
"pending": true,
|
||||
"pending": false,
|
||||
"pendingData": {
|
||||
"redeemTx": "cHNidP8BAIkCAAAAAWwxXUjG5tidFA0LmUljX//jwW6xWaS6HKyRCw5StsxpAAAAAAD/////AugDAAAAAAAAIlEgt2eR8LtqTP7yUcQtSydeGrRiHnVmHHnZwYjdC23G7MZwSQAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFAAAAAAABASsgTgAAAAAAACJRIKfUzf/o9h+r0v9y4nmyOt9qO8EkDumQPQZGTbEv8fSFIgYDp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShcYAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRSvBIBey3T0IV353FkuGLMmMJFpqHTrliIsJwJsfJkzq7J0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHQAFqkBLiRmP3AZ8MS77s1QIWZswMV3L72D9gN0f0MbD6XHkmzZeC1clF3uzxr+13wsF0vcFe29Zl3e2gAhMNGYVCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wKRtST8P7teUpSF4DAEbfJj5OIXITx5QGbZns/AtxqGyRSCn2zP0K2jsWEX4L3b1j+MnDXORFbGro1RF32RfTmZKF60grwSAXst09CFd+dxZLhizJjCRaah065YiLCcCbHyZM6uswCEWp9sz9Cto7FhF+C929Y/jJw1zkRWxq6NURd9kX05mShc5AbJ0B8bQ0j9842h5lUfOWcbj2TeoFx6OCpgoHIqWIBhHAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=",
|
||||
"unconditionalForfeitTxs": [
|
||||
@@ -527,7 +479,7 @@ var (
|
||||
"spentBy": "",
|
||||
"expireAt": "1726503895",
|
||||
"swept": false,
|
||||
"pending": true,
|
||||
"pending": false,
|
||||
"pendingData": {
|
||||
"redeemTx": "cHNidP8BAIkCAAAAAdOK9YzYw1ceJznqJxtRXGe0KeHj6CLcLtqLVwcbMCivAAAAAAD/////ArgLAAAAAAAAIlEgC39Vxhw3dIa4heHgFS6X4XwDl1mBggsKLVTBwF1h3qEgegEAAAAAACJRIMkktfIFxFNTtAmy3K0p+7JqVn2kcA0P6y2vJ1QX2zysAAAAAAABASughgEAAAAAACJRIMkktfIFxFNTtAmy3K0p+7JqVn2kcA0P6y2vJ1QX2zysIgYDjGeMfnNwCrU45iB3iRqiFdWTADaiJ968+w3ruFuq1F0YAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAQRTYEOuHJ0hyLBGzY8nSHpD2F1nby5/XQ5Sh2Je+cQ5Wsx0ZucLmB/LLspxMRN9JcJn3Q2KJRMhhg7415cCg1d0gQNSvgaBk/1WLYqQxCKxCfv8ViVJ7vjBxvNO5tc2FEDy27V9cIrfL1jPJoVrhgPZT0GwY7dkVZS7saIKI03CbipBCFcBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wPKiQ0JM6aw2kcUByijEbOydM3gTIVCGN/69q+dmyxcqRSCMZ4x+c3AKtTjmIHeJGqIV1ZMANqIn3rz7Deu4W6rUXa0g2BDrhydIciwRs2PJ0h6Q9hdZ28uf10OUodiXvnEOVrOswCEWjGeMfnNwCrU45iB3iRqiFdWTADaiJ968+w3ruFuq1F05AR0ZucLmB/LLspxMRN9JcJn3Q2KJRMhhg7415cCg1d0gAAAAAFYAAIAAAACAAQAAgAAAAAAAAAAAARcgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAAAAA=",
|
||||
"unconditionalForfeitTxs": [
|
||||
|
||||
@@ -727,19 +727,39 @@ func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) {
|
||||
if a.StoreData == nil {
|
||||
return nil, fmt.Errorf("client not initialized")
|
||||
}
|
||||
|
||||
spendableVtxos, spentVtxos, err := a.ListVtxos(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := a.store.GetData(ctx)
|
||||
boardingTxs, ignoreVtxos, err := a.getBoardingTxs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
boardingTxs := a.getBoardingTxs(ctx)
|
||||
offchainTxs, err := vtxosToTxsCovenantless(
|
||||
a.StoreData.RoundLifetime, spendableVtxos, spentVtxos, ignoreVtxos,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vtxosToTxsCovenantless(config.RoundLifetime, spendableVtxos, spentVtxos, boardingTxs)
|
||||
txs := append(boardingTxs, offchainTxs...)
|
||||
// Sort the slice by age
|
||||
sort.Slice(txs, func(i, j int) bool {
|
||||
txi := txs[i]
|
||||
txj := txs[j]
|
||||
if txi.CreatedAt.Equal(txj.CreatedAt) {
|
||||
return txi.Type > txj.Type
|
||||
}
|
||||
return txi.CreatedAt.Before(txj.CreatedAt)
|
||||
})
|
||||
|
||||
return txs, nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) sendOnchain(
|
||||
@@ -1746,6 +1766,12 @@ func (a *covenantlessArkClient) getRedeemBranches(
|
||||
|
||||
for i := range vtxos {
|
||||
vtxo := vtxos[i]
|
||||
|
||||
// TODO: handle exit for pending changes
|
||||
if vtxo.RedeemTx != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := congestionTrees[vtxo.RoundTxid]; !ok {
|
||||
round, err := a.client.GetRound(ctx, vtxo.RoundTxid)
|
||||
if err != nil {
|
||||
@@ -1798,21 +1824,31 @@ func (a *covenantlessArkClient) getOffchainBalance(
|
||||
return balance, amountByExpiration, nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
|
||||
func (a *covenantlessArkClient) getAllBoardingUtxos(
|
||||
ctx context.Context,
|
||||
) ([]explorer.Utxo, map[string]struct{}, error) {
|
||||
_, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
utxos := []explorer.Utxo{}
|
||||
ignoreVtxos := make(map[string]struct{}, 0)
|
||||
for _, addr := range boardingAddrs {
|
||||
txs, err := a.explorer.GetTxs(addr)
|
||||
if err != nil {
|
||||
continue
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, tx := range txs {
|
||||
for i, vout := range tx.Vout {
|
||||
if vout.Address == addr {
|
||||
spentStatuses, err := a.explorer.GetTxOutspends(tx.Txid)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if s := spentStatuses[i]; s.Spent {
|
||||
ignoreVtxos[s.SpentBy] = struct{}{}
|
||||
}
|
||||
createdAt := time.Time{}
|
||||
if tx.Status.Confirmed {
|
||||
createdAt = time.Unix(tx.Status.Blocktime, 0)
|
||||
@@ -1828,7 +1864,7 @@ func (a *covenantlessArkClient) getAllBoardingUtxos(ctx context.Context) ([]expl
|
||||
}
|
||||
}
|
||||
|
||||
return utxos, nil
|
||||
return utxos, ignoreVtxos, nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
|
||||
@@ -1892,7 +1928,7 @@ func (a *covenantlessArkClient) getVtxos(
|
||||
|
||||
pendingVtxos := make([]client.Vtxo, 0)
|
||||
for _, vtxo := range spendableVtxos {
|
||||
if vtxo.Pending {
|
||||
if vtxo.RedeemTx != "" {
|
||||
pendingVtxos = append(pendingVtxos, vtxo)
|
||||
}
|
||||
}
|
||||
@@ -2017,10 +2053,19 @@ func (a *covenantlessArkClient) offchainAddressToDefaultVtxoDescriptor(addr stri
|
||||
return vtxoScript.ToDescriptor(), nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) {
|
||||
// getBoardingTxs builds the boarding tx history from onchain utxos:
|
||||
// - unspent utxo => pending boarding tx
|
||||
// - spent utxo => claimed boarding tx
|
||||
//
|
||||
// The tx spending an onchain utxo is an ark round, therefore an indexed list
|
||||
// of round txids is returned to specify the vtxos to be ignored to build the
|
||||
// offchain tx history and prevent duplicates.
|
||||
func (a *covenantlessArkClient) getBoardingTxs(
|
||||
ctx context.Context,
|
||||
) ([]Transaction, map[string]struct{}, error) {
|
||||
utxos, err := a.getClaimableBoardingUtxos(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
isPending := make(map[string]bool)
|
||||
@@ -2028,25 +2073,37 @@ func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transaction
|
||||
isPending[u.Txid] = true
|
||||
}
|
||||
|
||||
allUtxos, err := a.getAllBoardingUtxos(ctx)
|
||||
allUtxos, ignoreVtxos, err := a.getAllBoardingUtxos(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
unconfirmedTxs := make([]Transaction, 0)
|
||||
confirmedTxs := make([]Transaction, 0)
|
||||
for _, u := range allUtxos {
|
||||
pending := false
|
||||
if isPending[u.Txid] {
|
||||
pending = true
|
||||
}
|
||||
transactions = append(transactions, Transaction{
|
||||
|
||||
tx := Transaction{
|
||||
BoardingTxid: u.Txid,
|
||||
Amount: u.Amount,
|
||||
Type: TxReceived,
|
||||
IsPending: pending,
|
||||
CreatedAt: u.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
emptyTime := time.Time{}
|
||||
if u.CreatedAt == emptyTime {
|
||||
unconfirmedTxs = append(unconfirmedTxs, tx)
|
||||
continue
|
||||
}
|
||||
confirmedTxs = append(confirmedTxs, tx)
|
||||
}
|
||||
return
|
||||
|
||||
txs := append(unconfirmedTxs, confirmedTxs...)
|
||||
return txs, ignoreVtxos, nil
|
||||
}
|
||||
|
||||
func findVtxosBySpentBy(allVtxos []client.Vtxo, txid string) (vtxos []client.Vtxo) {
|
||||
@@ -2059,79 +2116,98 @@ func findVtxosBySpentBy(allVtxos []client.Vtxo, txid string) (vtxos []client.Vtx
|
||||
}
|
||||
|
||||
func vtxosToTxsCovenantless(
|
||||
roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []Transaction,
|
||||
roundLifetime int64, spendable, spent []client.Vtxo, ignoreVtxos map[string]struct{},
|
||||
) ([]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)
|
||||
|
||||
indexedTxs := make(map[string]Transaction)
|
||||
for _, v := range spent {
|
||||
// If the vtxo was pending and is spent => it's been claimed.
|
||||
if v.Pending {
|
||||
transactions = append(transactions, Transaction{
|
||||
RedeemTxid: v.Txid,
|
||||
Amount: v.Amount,
|
||||
Type: TxReceived,
|
||||
IsPending: false,
|
||||
CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt),
|
||||
})
|
||||
// Delete any duplicate in the indexed list.
|
||||
delete(indexedTxs, v.SpentBy)
|
||||
// Ignore the spendable vtxo created by the claim.
|
||||
ignoreVtxos[v.SpentBy] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
// If this vtxo spent another one => subtract the amount to find the sent amount.
|
||||
if tx, ok := indexedTxs[v.Txid]; ok {
|
||||
tx.Amount -= v.Amount
|
||||
if v.RedeemTx == "" {
|
||||
tx.RedeemTxid = ""
|
||||
} else {
|
||||
tx.RoundTxid = ""
|
||||
}
|
||||
indexedTxs[v.Txid] = tx
|
||||
}
|
||||
|
||||
// Add a transaction to the indexed list if not existing, it will be deleted if it's a duplicate.
|
||||
tx, ok := indexedTxs[v.SpentBy]
|
||||
if !ok {
|
||||
indexedTxs[v.SpentBy] = Transaction{
|
||||
RedeemTxid: v.SpentBy,
|
||||
RoundTxid: v.SpentBy,
|
||||
Amount: v.Amount,
|
||||
Type: TxSent,
|
||||
IsPending: false,
|
||||
CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise add the amount of this vtxo to the one of the tx in the indexed list.
|
||||
tx.Amount += v.Amount
|
||||
indexedTxs[v.SpentBy] = tx
|
||||
}
|
||||
|
||||
for _, v := range spendable {
|
||||
_, ok1 := ignoreVtxos[v.Txid]
|
||||
_, ok2 := ignoreVtxos[v.RoundTxid]
|
||||
if ok1 || ok2 {
|
||||
continue
|
||||
}
|
||||
txid := v.RoundTxid
|
||||
if txid == "" {
|
||||
txid = v.Txid
|
||||
}
|
||||
|
||||
tx, ok := indexedTxs[txid]
|
||||
if !ok {
|
||||
redeemTxid := ""
|
||||
if v.RoundTxid == "" {
|
||||
redeemTxid = v.Txid
|
||||
}
|
||||
transactions = append(transactions, Transaction{
|
||||
RedeemTxid: redeemTxid,
|
||||
RoundTxid: v.RoundTxid,
|
||||
Amount: v.Amount,
|
||||
Type: TxReceived,
|
||||
IsPending: v.Pending,
|
||||
CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
tx.Amount -= v.Amount
|
||||
if v.RedeemTx == "" {
|
||||
tx.RedeemTxid = ""
|
||||
} else {
|
||||
tx.RoundTxid = ""
|
||||
}
|
||||
indexedTxs[txid] = tx
|
||||
}
|
||||
|
||||
for _, tx := range indexedTxs {
|
||||
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
|
||||
}
|
||||
|
||||
// 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,
|
||||
IsPending: (v.Pending && len(v.SpentBy) == 0),
|
||||
IsPendingChange: v.PendingChange,
|
||||
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
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
@@ -80,6 +80,11 @@ type ExplorerUtxo struct {
|
||||
} `json:"status"`
|
||||
}
|
||||
|
||||
type SpentStatus struct {
|
||||
Spent bool `json:"spent"`
|
||||
SpentBy string `json:"txid,omitempty"`
|
||||
}
|
||||
|
||||
func (e ExplorerUtxo) ToUtxo(delay uint) Utxo {
|
||||
return newUtxo(e, delay)
|
||||
}
|
||||
@@ -88,6 +93,7 @@ type Explorer interface {
|
||||
GetTxHex(txid string) (string, error)
|
||||
Broadcast(txHex string) (string, error)
|
||||
GetTxs(addr string) ([]ExplorerTx, error)
|
||||
GetTxOutspends(tx string) ([]SpentStatus, error)
|
||||
GetUtxos(addr string) ([]ExplorerUtxo, error)
|
||||
GetBalance(addr string) (uint64, error)
|
||||
GetRedeemedVtxosBalance(
|
||||
@@ -215,6 +221,28 @@ func (e *explorerSvc) GetTxs(addr string) ([]ExplorerTx, error) {
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (e *explorerSvc) GetTxOutspends(txid string) ([]SpentStatus, error) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/tx/%s/outspends", e.baseUrl, txid))
|
||||
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))
|
||||
}
|
||||
|
||||
spentStatuses := make([]SpentStatus, 0)
|
||||
if err := json.Unmarshal(body, &spentStatuses); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return spentStatuses, nil
|
||||
}
|
||||
|
||||
func (e *explorerSvc) GetUtxos(addr string) ([]ExplorerUtxo, error) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", e.baseUrl, addr))
|
||||
if err != nil {
|
||||
|
||||
@@ -131,12 +131,11 @@ const (
|
||||
type TxType string
|
||||
|
||||
type Transaction struct {
|
||||
BoardingTxid string
|
||||
RoundTxid string
|
||||
RedeemTxid string
|
||||
Amount uint64
|
||||
Type TxType
|
||||
IsPending bool
|
||||
IsPendingChange bool
|
||||
CreatedAt time.Time
|
||||
BoardingTxid string
|
||||
RoundTxid string
|
||||
RedeemTxid string
|
||||
Amount uint64
|
||||
Type TxType
|
||||
IsPending bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user