Skip to content

Commit 4b44911

Browse files
committed
pr review
1 parent 5a47ad3 commit 4b44911

File tree

9 files changed

+331
-25
lines changed

9 files changed

+331
-25
lines changed

server/internal/core/application/indexer.go

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,13 @@ func (i *indexerService) GetTransactionHistory(ctx context.Context, req TxHistor
213213
}, nil
214214
}
215215

216+
type vtxoKeyWithCreatedAt struct {
217+
domain.VtxoKey
218+
CreatedAt int64
219+
}
220+
216221
func (i *indexerService) GetVtxoChain(ctx context.Context, req VtxoChainReq) (*VtxoChainResp, error) {
217-
chainMap := make(map[Outpoint]domain.Vtxo)
222+
chainMap := make(map[vtxoKeyWithCreatedAt][]string)
218223

219224
outpoint := domain.VtxoKey{
220225
Txid: req.VtxoKey.Txid,
@@ -225,24 +230,20 @@ func (i *indexerService) GetVtxoChain(ctx context.Context, req VtxoChainReq) (*V
225230
return nil, err
226231
}
227232

228-
chainSlice := make([]domain.Vtxo, 0, len(chainMap))
229-
for _, vtxo := range chainMap {
233+
chainSlice := make([]vtxoKeyWithCreatedAt, 0, len(chainMap))
234+
for vtxo := range chainMap {
230235
chainSlice = append(chainSlice, vtxo)
231236
}
232237

233238
sort.Slice(chainSlice, func(i, j int) bool {
234239
return chainSlice[i].CreatedAt > chainSlice[j].CreatedAt
235240
})
236241

237-
pagedVtxos, pageResp := paginate(chainSlice, req.Page, maxPageSizeVtxoChain)
242+
pagedChainSlice, pageResp := paginate(chainSlice, req.Page, maxPageSizeVtxoChain)
238243

239-
txMap := make(map[Outpoint]string, len(pagedVtxos))
240-
for _, vtxo := range pagedVtxos {
241-
out := Outpoint{
242-
Txid: vtxo.Txid,
243-
Vout: vtxo.VOut,
244-
}
245-
txMap[out] = vtxo.RedeemTx
244+
txMap := make(map[string][]string)
245+
for _, vtxo := range pagedChainSlice {
246+
txMap[vtxo.Txid] = chainMap[vtxo]
246247
}
247248

248249
return &VtxoChainResp{
@@ -254,7 +255,7 @@ func (i *indexerService) GetVtxoChain(ctx context.Context, req VtxoChainReq) (*V
254255
func (i *indexerService) buildChain(
255256
ctx context.Context,
256257
outpoint domain.VtxoKey,
257-
chain map[Outpoint]domain.Vtxo,
258+
chain map[vtxoKeyWithCreatedAt][]string,
258259
isFirst bool,
259260
) error {
260261
vtxos, err := i.repoManager.Vtxos().GetVtxos(ctx, []domain.VtxoKey{outpoint})
@@ -267,10 +268,19 @@ func (i *indexerService) buildChain(
267268
}
268269

269270
vtxo := vtxos[0]
271+
key := vtxoKeyWithCreatedAt{
272+
VtxoKey: outpoint,
273+
CreatedAt: vtxo.CreatedAt,
274+
}
275+
if _, ok := chain[key]; !ok {
276+
chain[key] = make([]string, 0)
277+
} else {
278+
return nil
279+
}
270280

271-
chain[Outpoint{Txid: vtxo.Txid, Vout: vtxo.VOut}] = vtxo
272-
281+
//finish chain if this is the leaf Vtxo
273282
if !vtxo.IsPending() {
283+
chain[key] = append(chain[key], vtxo.RoundTxid)
274284
return nil
275285
}
276286

@@ -279,13 +289,19 @@ func (i *indexerService) buildChain(
279289
return err
280290
}
281291

282-
parentIn := redeemPsbt.UnsignedTx.TxIn[0]
283-
parentOutpoint := domain.VtxoKey{
284-
Txid: parentIn.PreviousOutPoint.Hash.String(),
285-
VOut: parentIn.PreviousOutPoint.Index,
292+
for _, in := range redeemPsbt.UnsignedTx.TxIn {
293+
chain[key] = append(chain[key], in.PreviousOutPoint.Hash.String())
294+
parentOutpoint := domain.VtxoKey{
295+
Txid: in.PreviousOutPoint.Hash.String(),
296+
VOut: in.PreviousOutPoint.Index,
297+
}
298+
299+
if err := i.buildChain(ctx, parentOutpoint, chain, false); err != nil {
300+
return err
301+
}
286302
}
287303

288-
return i.buildChain(ctx, parentOutpoint, chain, false)
304+
return nil
289305
}
290306

291307
func (i *indexerService) GetVirtualTxs(ctx context.Context, req VirtualTxsReq) (*VirtualTxsResp, error) {
@@ -314,6 +330,14 @@ func (i *indexerService) GetVirtualTxs(ctx context.Context, req VirtualTxsReq) (
314330
}
315331
}
316332

333+
vtxs, err := i.repoManager.Rounds().GetTxsWithTxids(ctx, req.TxIDs)
334+
if err != nil {
335+
return nil, err
336+
}
337+
for _, vtx := range vtxs {
338+
txs = append(txs, vtx)
339+
}
340+
317341
virtualTxs, reps := paginate(txs, req.Page, maxPageSizeVirtualTxs)
318342

319343
return &VirtualTxsResp{
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package application
2+
3+
import (
4+
"context"
5+
"github.com/btcsuite/btcd/btcutil/psbt"
6+
"github.com/stretchr/testify/assert"
7+
"testing"
8+
"time"
9+
10+
"github.com/ark-network/ark/server/internal/core/domain" // adapt for your project
11+
"github.com/btcsuite/btcd/chaincfg/chainhash"
12+
"github.com/btcsuite/btcd/wire"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
//---------------------------------------------
17+
// Test scenario:
18+
//
19+
// LeafTx (outpoint = vtxoA) [leaf, no RedeemTx]
20+
// \
21+
// \---> RedeemTx1 (inputs: A) => produces vtxoB, vtxoC
22+
// \
23+
// \---> RedeemTx2 (inputs: B) => produces vtxoD, vtxoE
24+
// \
25+
// \---> RedeemTx3 (inputs: C, E) => produces vtxoF (final)
26+
//---------------------------------------------
27+
28+
func TestBuildChain(t *testing.T) {
29+
//TODO: test more complex scenarios
30+
ctx := context.Background()
31+
32+
//
33+
// 1) Build all the PSBTs needed
34+
//
35+
// Leaf: vtxoA is not pending => no RedeemTx. It's identified by (Txid="leafTx", VOut=0).
36+
// RedeemTx1 => references [ (leafTx,0) ].
37+
redeemTx1, redeemTx1ID, err := makePsbtReferencingMany([]domain.VtxoKey{
38+
{Txid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", VOut: 0},
39+
})
40+
require.NoError(t, err)
41+
42+
// RedeemTx2 => references [ (RedeemTx1,0) ] (i.e. vtxoB).
43+
redeemTx2, redeemTx2ID, err := makePsbtReferencingMany([]domain.VtxoKey{
44+
{Txid: redeemTx1ID, VOut: 0},
45+
})
46+
require.NoError(t, err)
47+
48+
// RedeemTx3 => references [ (RedeemTx1,1), (RedeemTx2,1) ] (i.e. vtxoC, vtxoE).
49+
redeemTx3, redeemTx3ID, err := makePsbtReferencingMany([]domain.VtxoKey{
50+
{Txid: redeemTx1ID, VOut: 0},
51+
{Txid: redeemTx2ID, VOut: 1},
52+
})
53+
require.NoError(t, err)
54+
55+
mockData := map[domain.VtxoKey]domain.Vtxo{
56+
{Txid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", VOut: 0}: {
57+
VtxoKey: domain.VtxoKey{Txid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", VOut: 0},
58+
RoundTxid: "roundTxid",
59+
CreatedAt: time.Now().Add(-5 * time.Hour).Unix(),
60+
},
61+
{Txid: redeemTx1ID, VOut: 0}: {
62+
VtxoKey: domain.VtxoKey{Txid: redeemTx1ID, VOut: 0},
63+
RedeemTx: redeemTx1,
64+
CreatedAt: time.Now().Add(-4 * time.Hour).Unix(),
65+
},
66+
{Txid: redeemTx1ID, VOut: 1}: {
67+
VtxoKey: domain.VtxoKey{Txid: redeemTx1ID, VOut: 1},
68+
RedeemTx: redeemTx1,
69+
CreatedAt: time.Now().Add(-4 * time.Hour).Unix(),
70+
},
71+
72+
{Txid: redeemTx2ID, VOut: 0}: {
73+
VtxoKey: domain.VtxoKey{Txid: redeemTx2ID, VOut: 0},
74+
RedeemTx: redeemTx2,
75+
CreatedAt: time.Now().Add(-3 * time.Hour).Unix(),
76+
},
77+
{Txid: redeemTx2ID, VOut: 1}: {
78+
VtxoKey: domain.VtxoKey{Txid: redeemTx2ID, VOut: 1},
79+
RedeemTx: redeemTx2,
80+
CreatedAt: time.Now().Add(-3 * time.Hour).Unix(),
81+
},
82+
83+
{Txid: redeemTx3ID, VOut: 0}: {
84+
VtxoKey: domain.VtxoKey{Txid: redeemTx3ID, VOut: 0},
85+
RedeemTx: redeemTx3,
86+
CreatedAt: time.Now().Add(-1 * time.Hour).Unix(),
87+
},
88+
}
89+
90+
vtxoRepo := &MockVtxoRepo{data: mockData}
91+
repoManager := &MockRepoManager{vtxoRepo: vtxoRepo}
92+
svc := indexerService{repoManager: repoManager}
93+
94+
resp, err := svc.GetVtxoChain(
95+
ctx, VtxoChainReq{
96+
VtxoKey: Outpoint{
97+
Txid: redeemTx3ID,
98+
Vout: 0,
99+
},
100+
Page: PageReq{},
101+
},
102+
)
103+
require.NoError(t, err, "buildChain should succeed")
104+
105+
redeemTx3Txs := resp.Transactions[redeemTx3ID]
106+
assert.Equal(t, len(redeemTx3Txs), 2)
107+
assert.Equal(t, redeemTx3Txs[0], redeemTx1ID)
108+
assert.Equal(t, redeemTx3Txs[1], redeemTx2ID)
109+
110+
redeemTx2Txs := resp.Transactions[redeemTx2ID]
111+
assert.Equal(t, len(redeemTx2Txs), 1)
112+
assert.Equal(t, redeemTx2Txs[0], redeemTx1ID)
113+
114+
redeemTx1Txs := resp.Transactions[redeemTx1ID]
115+
assert.Equal(t, len(redeemTx1Txs), 1)
116+
assert.Equal(t, redeemTx1Txs[0], "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
117+
118+
leafTxTxs := resp.Transactions["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
119+
assert.Equal(t, len(leafTxTxs), 1)
120+
assert.Equal(t, leafTxTxs[0], "roundTxid")
121+
}
122+
123+
type MockRepoManager struct {
124+
vtxoRepo *MockVtxoRepo
125+
}
126+
127+
func (m *MockRepoManager) Events() domain.RoundEventRepository { panic("not implemented") }
128+
func (m *MockRepoManager) Rounds() domain.RoundRepository { panic("not implemented") }
129+
func (m *MockRepoManager) Vtxos() domain.VtxoRepository { return m.vtxoRepo }
130+
func (m *MockRepoManager) Notes() domain.NoteRepository { panic("not implemented") }
131+
func (m *MockRepoManager) Entities() domain.EntityRepository { panic("not implemented") }
132+
func (m *MockRepoManager) MarketHourRepo() domain.MarketHourRepo { panic("not implemented") }
133+
func (m *MockRepoManager) RegisterEventsHandler(func(*domain.Round)) { panic("not implemented") }
134+
func (m *MockRepoManager) Close() {}
135+
136+
type MockVtxoRepo struct {
137+
data map[domain.VtxoKey]domain.Vtxo
138+
}
139+
140+
func (m *MockVtxoRepo) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) error {
141+
panic("not implemented")
142+
}
143+
func (m *MockVtxoRepo) SpendVtxos(ctx context.Context, vtxos []domain.VtxoKey, txid string) error {
144+
panic("not implemented")
145+
}
146+
func (m *MockVtxoRepo) RedeemVtxos(ctx context.Context, vtxos []domain.VtxoKey) error {
147+
panic("not implemented")
148+
}
149+
func (m *MockVtxoRepo) GetVtxos(ctx context.Context, vtxos []domain.VtxoKey) ([]domain.Vtxo, error) {
150+
var out []domain.Vtxo
151+
for _, k := range vtxos {
152+
if v, ok := m.data[k]; ok {
153+
out = append(out, v)
154+
}
155+
}
156+
return out, nil
157+
}
158+
func (m *MockVtxoRepo) GetVtxosForRound(ctx context.Context, txid string) ([]domain.Vtxo, error) {
159+
panic("not implemented")
160+
}
161+
func (m *MockVtxoRepo) SweepVtxos(ctx context.Context, vtxos []domain.VtxoKey) error {
162+
panic("not implemented")
163+
}
164+
func (m *MockVtxoRepo) GetAllNonRedeemedVtxos(ctx context.Context, pubkey string) ([]domain.Vtxo, []domain.Vtxo, error) {
165+
panic("not implemented")
166+
}
167+
func (m *MockVtxoRepo) GetAllSweepableVtxos(ctx context.Context) ([]domain.Vtxo, error) {
168+
panic("not implemented")
169+
}
170+
func (m *MockVtxoRepo) GetAll(ctx context.Context) ([]domain.Vtxo, error) { panic("not implemented") }
171+
func (m *MockVtxoRepo) UpdateExpireAt(ctx context.Context, vtxos []domain.VtxoKey, expireAt int64) error {
172+
panic("not implemented")
173+
}
174+
func (m *MockVtxoRepo) Close() {}
175+
176+
func makePsbtReferencingMany(parents []domain.VtxoKey) (string, string, error) {
177+
tx := wire.NewMsgTx(wire.TxVersion)
178+
for _, p := range parents {
179+
parentHash, err := chainhash.NewHashFromStr(p.Txid)
180+
if err != nil {
181+
return "", "", err
182+
}
183+
txIn := wire.NewTxIn(wire.NewOutPoint(parentHash, p.VOut), nil, nil)
184+
tx.AddTxIn(txIn)
185+
}
186+
187+
tx.AddTxOut(wire.NewTxOut(1000, []byte{}))
188+
189+
p, err := psbt.NewFromUnsignedTx(tx)
190+
if err != nil {
191+
return "", "", err
192+
}
193+
194+
ptxHex, err := p.B64Encode()
195+
if err != nil {
196+
return "", "", err
197+
}
198+
199+
return ptxHex, tx.TxHash().String(), nil
200+
}

server/internal/core/application/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ type VtxoChainReq struct {
169169
}
170170

171171
type VtxoChainResp struct {
172-
Transactions map[Outpoint]string
172+
Transactions map[string][]string
173173
Page PageResp
174174
}
175175

server/internal/core/domain/round_repo.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type RoundRepository interface {
2121
GetExpiredRoundsTxid(ctx context.Context) ([]string, error)
2222
GetRoundsIds(ctx context.Context, startedAfter int64, startedBefore int64) ([]string, error)
2323
GetSweptRoundsConnectorAddress(ctx context.Context) ([]string, error)
24+
GetTxsWithTxids(ctx context.Context, txids []string) ([]string, error)
2425
Close()
2526
}
2627

server/internal/infrastructure/db/badger/round_repo.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ func (r *roundRepository) Close() {
153153
r.store.Close()
154154
}
155155

156+
func (r *roundRepository) GetTxsWithTxids(ctx context.Context, txids []string) ([]string, error) {
157+
//TODO implement me
158+
panic("implement me")
159+
}
160+
156161
func (r *roundRepository) findRound(
157162
ctx context.Context, query *badgerhold.Query,
158163
) ([]domain.Round, error) {

server/internal/infrastructure/db/sqlite/round_repo.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,27 @@ func (r *roundRepository) GetVtxoTreeWithTxid(ctx context.Context, txid string)
271271
return vtxoTree, nil
272272
}
273273

274+
func (r *roundRepository) GetTxsWithTxids(ctx context.Context, txids []string) ([]string, error) {
275+
txs := make([]sql.NullString, 0, len(txids))
276+
for _, txid := range txids {
277+
txs = append(txs, sql.NullString{
278+
String: txid,
279+
Valid: true,
280+
})
281+
}
282+
rows, err := r.querier.GetTxsByTxid(ctx, txs)
283+
if err != nil {
284+
return nil, err
285+
}
286+
287+
resp := make([]string, 0, len(rows))
288+
for _, row := range rows {
289+
resp = append(resp, row.Tx.Tx)
290+
}
291+
292+
return resp, nil
293+
}
294+
274295
func rowToReceiver(row queries.RequestReceiverVw) domain.Receiver {
275296
return domain.Receiver{
276297
Amount: uint64(row.Amount.Int64),

0 commit comments

Comments
 (0)