Skip to content

Commit f756dc3

Browse files
authored
[SDK] Detect rbf txs while listening for boarding utxos (#495)
* Drop explorer url from sdk * SDK: Add support for rbf txs * Fix history order * Add debug log * Add tx hex to msgs sent on transaction stream * Fix * Add more logs * Revert "Add more logs" This reverts commit 77bab59. * Revert "Add debug log" This reverts commit d8614a0. * Fix * Fix repo.RbfTransactions and add test * Fixes * Add debug logs * Fixes & Lint * Fix wasm * Revert "Drop explorer url from sdk" This reverts commit 6bd6f28. * Revert "Fix wasm" This reverts commit d1c6b0d. * Fix
1 parent daf9f2a commit f756dc3

File tree

19 files changed

+549
-223
lines changed

19 files changed

+549
-223
lines changed

api-spec/openapi/swagger/ark/v1/service.swagger.json

+6
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,9 @@
689689
"type": "object",
690690
"$ref": "#/definitions/v1Vtxo"
691691
}
692+
},
693+
"hex": {
694+
"type": "string"
692695
}
693696
}
694697
},
@@ -847,6 +850,9 @@
847850
"type": "object",
848851
"$ref": "#/definitions/v1Outpoint"
849852
}
853+
},
854+
"hex": {
855+
"type": "string"
850856
}
851857
}
852858
},

api-spec/protobuf/ark/v1/types.proto

+2
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,14 @@ message RoundTransaction {
7474
repeated Vtxo spent_vtxos = 2;
7575
repeated Vtxo spendable_vtxos = 3;
7676
repeated Outpoint claimed_boarding_utxos = 4;
77+
string hex = 5;
7778
}
7879

7980
message RedeemTransaction {
8081
string txid = 1;
8182
repeated Vtxo spent_vtxos = 2;
8283
repeated Vtxo spendable_vtxos = 3;
84+
string hex = 4;
8385
}
8486

8587
// This message is used to prove to the server that the user controls the vtxo without revealing the whole VTXO taproot tree.

api-spec/protobuf/gen/ark/v1/types.pb.go

+163-145
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/client-sdk/client.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ var (
4545
common.Liquid.Name: "https://blockstream.info/liquid/api",
4646
common.LiquidTestNet.Name: "https://blockstream.info/liquidtestnet/api",
4747
common.LiquidRegTest.Name: "http://localhost:3001",
48-
common.Bitcoin.Name: "https://blockstream.info/api",
49-
common.BitcoinTestNet.Name: "https://blockstream.info/testnet/api",
48+
common.Bitcoin.Name: "https://mempool.space/api",
49+
common.BitcoinTestNet.Name: "https://mempool.space/testnet/api",
5050
//common.BitcoinTestNet4.Name: "https://mempool.space/testnet4/api", //TODO uncomment once supported
51-
common.BitcoinSigNet.Name: "https://blockstream.info/signet/api",
51+
common.BitcoinSigNet.Name: "https://mempool.space/signet/api",
5252
common.BitcoinMutinyNet.Name: "https://mutinynet.com/api",
5353
common.BitcoinRegTest.Name: "http://localhost:3000",
5454
}

pkg/client-sdk/client/client.go

+2
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,14 @@ type RoundTransaction struct {
224224
SpentVtxos []Vtxo
225225
SpendableVtxos []Vtxo
226226
ClaimedBoardingUtxos []Outpoint
227+
Hex string
227228
}
228229

229230
type RedeemTransaction struct {
230231
Txid string
231232
SpentVtxos []Vtxo
232233
SpendableVtxos []Vtxo
234+
Hex string
233235
}
234236

235237
type SignedVtxoOutpoint struct {

pkg/client-sdk/client/grpc/client.go

+2
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ func (c *grpcClient) GetTransactionsStream(
368368
SpentVtxos: vtxos(tx.Round.SpentVtxos).toVtxos(),
369369
SpendableVtxos: vtxos(tx.Round.SpendableVtxos).toVtxos(),
370370
ClaimedBoardingUtxos: outpointsFromProto(tx.Round.ClaimedBoardingUtxos),
371+
Hex: tx.Round.GetHex(),
371372
},
372373
}
373374
case *arkv1.GetTransactionsStreamResponse_Redeem:
@@ -376,6 +377,7 @@ func (c *grpcClient) GetTransactionsStream(
376377
Txid: tx.Redeem.Txid,
377378
SpentVtxos: vtxos(tx.Redeem.SpentVtxos).toVtxos(),
378379
SpendableVtxos: vtxos(tx.Redeem.SpendableVtxos).toVtxos(),
380+
Hex: tx.Redeem.GetHex(),
379381
},
380382
}
381383
}

pkg/client-sdk/client/rest/service/models/v1_redeem_transaction.go

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/client-sdk/client/rest/service/models/v1_round_transaction.go

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/client-sdk/covenantless_client.go

+110-28
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,12 @@ func (a *covenantlessArkClient) listenForBoardingTxs(ctx context.Context) {
336336
for {
337337
select {
338338
case <-ticker.C:
339-
txsToAdd, txsToConfirm, err := a.getBoardingPendingTransactions(ctx)
339+
_, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
340+
if err != nil {
341+
log.WithError(err).Error("failed to get all boarding addresses")
342+
continue
343+
}
344+
txsToAdd, txsToConfirm, rbfTxs, err := a.getBoardingTransactions(ctx, boardingAddrs)
340345
if err != nil {
341346
log.WithError(err).Error("failed to get pending transactions")
342347
continue
@@ -361,36 +366,99 @@ func (a *covenantlessArkClient) listenForBoardingTxs(ctx context.Context) {
361366
log.WithError(err).Error("failed to update boarding transactions")
362367
continue
363368
}
364-
log.Debugf("updated %d boarding transaction(s)", count)
369+
log.Debugf("confirmed %d boarding transaction(s)", count)
370+
}
371+
372+
if len(rbfTxs) > 0 {
373+
count, err := a.store.TransactionStore().RbfTransactions(ctx, rbfTxs)
374+
if err != nil {
375+
log.WithError(err).Error("failed to update rbf boarding transactions")
376+
continue
377+
}
378+
log.Debugf("replaced %d transaction(s)", count)
365379
}
366380
case <-ctx.Done():
367381
return
368382
}
369383
}
370384
}
371385

372-
func (a *covenantlessArkClient) getBoardingPendingTransactions(
373-
ctx context.Context,
374-
) ([]types.Transaction, []string, error) {
386+
func (a *covenantlessArkClient) getBoardingTransactions(
387+
ctx context.Context, boardingAddrs []wallet.TapscriptsAddress,
388+
) ([]types.Transaction, []string, map[string]types.Transaction, error) {
375389
oldTxs, err := a.store.TransactionStore().GetAllTransactions(ctx)
376390
if err != nil {
377-
return nil, nil, err
391+
return nil, nil, nil, err
378392
}
379393

380-
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx, nil)
394+
rbfTxs := make(map[string]types.Transaction, 0)
395+
replacements := make(map[string]struct{}, 0)
396+
for _, tx := range oldTxs {
397+
if tx.IsBoarding() && tx.CreatedAt.IsZero() {
398+
isRbf, replacedBy, timestamp, err := a.explorer.IsRBFTx(tx.BoardingTxid, tx.Hex)
399+
if err != nil {
400+
return nil, nil, nil, err
401+
}
402+
if isRbf {
403+
txHex, err := a.explorer.GetTxHex(replacedBy)
404+
if err != nil {
405+
return nil, nil, nil, err
406+
}
407+
rawTx := &wire.MsgTx{}
408+
if err := rawTx.Deserialize(strings.NewReader(txHex)); err != nil {
409+
return nil, nil, nil, err
410+
}
411+
amount := uint64(0)
412+
netParams := utils.ToBitcoinNetwork(a.Network)
413+
for _, addr := range boardingAddrs {
414+
decoded, err := btcutil.DecodeAddress(addr.Address, &netParams)
415+
if err != nil {
416+
return nil, nil, nil, err
417+
}
418+
pkScript, err := txscript.PayToAddrScript(decoded)
419+
if err != nil {
420+
return nil, nil, nil, err
421+
}
422+
for _, out := range rawTx.TxOut {
423+
if bytes.Equal(out.PkScript, pkScript) {
424+
amount = uint64(out.Value)
425+
break
426+
}
427+
}
428+
if amount > 0 {
429+
break
430+
}
431+
}
432+
rbfTxs[tx.BoardingTxid] = types.Transaction{
433+
TransactionKey: types.TransactionKey{
434+
BoardingTxid: replacedBy,
435+
},
436+
CreatedAt: time.Unix(timestamp, 0),
437+
Hex: txHex,
438+
Amount: amount,
439+
}
440+
replacements[replacedBy] = struct{}{}
441+
}
442+
}
443+
}
444+
445+
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx, boardingAddrs, nil)
381446
if err != nil {
382-
return nil, nil, err
447+
return nil, nil, nil, err
383448
}
384449

385450
txsToAdd := make([]types.Transaction, 0)
386451
txsToConfirm := make([]string, 0)
387452
for _, u := range boardingUtxos {
453+
if _, ok := replacements[u.Txid]; ok {
454+
continue
455+
}
456+
388457
found := false
389458
for _, tx := range oldTxs {
390459
if tx.BoardingTxid == u.Txid {
391460
found = true
392-
emptyTime := time.Time{}
393-
if tx.CreatedAt == emptyTime && tx.CreatedAt != u.CreatedAt {
461+
if tx.CreatedAt.IsZero() && tx.CreatedAt != u.CreatedAt {
394462
txsToConfirm = append(txsToConfirm, tx.TransactionKey.String())
395463
}
396464
break
@@ -408,10 +476,11 @@ func (a *covenantlessArkClient) getBoardingPendingTransactions(
408476
Amount: u.Amount,
409477
Type: types.TxReceived,
410478
CreatedAt: u.CreatedAt,
479+
Hex: u.Tx,
411480
})
412481
}
413482

414-
return txsToAdd, txsToConfirm, nil
483+
return txsToAdd, txsToConfirm, rbfTxs, nil
415484
}
416485

417486
func (a *covenantlessArkClient) Balance(
@@ -877,7 +946,7 @@ func (a *covenantlessArkClient) CollaborativeExit(
877946
return "", fmt.Errorf("invalid onchain address")
878947
}
879948

880-
offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx)
949+
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
881950
if err != nil {
882951
return "", err
883952
}
@@ -911,7 +980,7 @@ func (a *covenantlessArkClient) CollaborativeExit(
911980
}
912981
}
913982

914-
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx, nil)
983+
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx, boardingAddrs, nil)
915984
if err != nil {
916985
return "", err
917986
}
@@ -1001,7 +1070,14 @@ func (a *covenantlessArkClient) GetTransactionHistory(
10011070
}
10021071

10031072
if a.Config.WithTransactionFeed {
1004-
return a.store.TransactionStore().GetAllTransactions(ctx)
1073+
history, err := a.store.TransactionStore().GetAllTransactions(ctx)
1074+
if err != nil {
1075+
return nil, err
1076+
}
1077+
sort.SliceStable(history, func(i, j int) bool {
1078+
return history[i].CreatedAt.IsZero() || history[i].CreatedAt.After(history[j].CreatedAt)
1079+
})
1080+
return history, nil
10051081
}
10061082

10071083
spendableVtxos, spentVtxos, err := a.ListVtxos(ctx)
@@ -1019,13 +1095,13 @@ func (a *covenantlessArkClient) GetTransactionHistory(
10191095
return nil, err
10201096
}
10211097

1022-
txs := append(boardingTxs, offchainTxs...)
1098+
history := append(boardingTxs, offchainTxs...)
10231099
// Sort the slice by age
1024-
sort.SliceStable(txs, func(i, j int) bool {
1025-
return txs[i].CreatedAt.After(txs[j].CreatedAt)
1100+
sort.SliceStable(history, func(i, j int) bool {
1101+
return history[i].CreatedAt.IsZero() || history[i].CreatedAt.After(history[j].CreatedAt)
10261102
})
10271103

1028-
return txs, nil
1104+
return history, nil
10291105
}
10301106

10311107
func (a *covenantlessArkClient) SetNostrNotificationRecipient(ctx context.Context, nostrProfile string) error {
@@ -1328,7 +1404,7 @@ func (a *covenantlessArkClient) sendOffchain(
13281404
sumOfReceivers += receiver.Amount()
13291405
}
13301406

1331-
offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx)
1407+
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
13321408
if err != nil {
13331409
return "", err
13341410
}
@@ -1360,7 +1436,7 @@ func (a *covenantlessArkClient) sendOffchain(
13601436
}
13611437
}
13621438

1363-
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx, nil)
1439+
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx, boardingAddrs, nil)
13641440
if err != nil {
13651441
return "", err
13661442
}
@@ -2331,6 +2407,10 @@ func (a *covenantlessArkClient) getAllBoardingUtxos(ctx context.Context) ([]type
23312407
for i, vout := range tx.Vout {
23322408
var spent bool
23332409
if vout.Address == addr.Address {
2410+
txHex, err := a.explorer.GetTxHex(tx.Txid)
2411+
if err != nil {
2412+
return nil, nil, err
2413+
}
23342414
spentStatuses, err := a.explorer.GetTxOutspends(tx.Txid)
23352415
if err != nil {
23362416
return nil, nil, err
@@ -2350,6 +2430,7 @@ func (a *covenantlessArkClient) getAllBoardingUtxos(ctx context.Context) ([]type
23502430
CreatedAt: createdAt,
23512431
Tapscripts: addr.Tapscripts,
23522432
Spent: spent,
2433+
Tx: txHex,
23532434
})
23542435
}
23552436
}
@@ -2359,12 +2440,9 @@ func (a *covenantlessArkClient) getAllBoardingUtxos(ctx context.Context) ([]type
23592440
return utxos, ignoreVtxos, nil
23602441
}
23612442

2362-
func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context, opts *CoinSelectOptions) ([]types.Utxo, error) {
2363-
_, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
2364-
if err != nil {
2365-
return nil, err
2366-
}
2367-
2443+
func (a *covenantlessArkClient) getClaimableBoardingUtxos(
2444+
_ context.Context, boardingAddrs []wallet.TapscriptsAddress, opts *CoinSelectOptions,
2445+
) ([]types.Utxo, error) {
23682446
claimable := make([]types.Utxo, 0)
23692447
for _, addr := range boardingAddrs {
23702448
boardingScript, err := bitcointree.ParseVtxoScript(addr.Tapscripts)
@@ -2525,10 +2603,10 @@ func (a *covenantlessArkClient) getBoardingTxs(
25252603
Type: types.TxReceived,
25262604
CreatedAt: u.CreatedAt,
25272605
Settled: u.Spent,
2606+
Hex: u.Tx,
25282607
}
25292608

2530-
emptyTime := time.Time{}
2531-
if u.CreatedAt == emptyTime {
2609+
if u.CreatedAt.IsZero() {
25322610
unconfirmedTxs = append(unconfirmedTxs, tx)
25332611
continue
25342612
}
@@ -2620,6 +2698,7 @@ func (a *covenantlessArkClient) handleRoundTx(
26202698
Type: types.TxReceived,
26212699
Settled: true,
26222700
CreatedAt: time.Now(),
2701+
Hex: roundTx.Hex,
26232702
})
26242703
} else {
26252704
vtxosToAddAmount := uint64(0)
@@ -2639,6 +2718,7 @@ func (a *covenantlessArkClient) handleRoundTx(
26392718
Type: types.TxSent,
26402719
Settled: true,
26412720
CreatedAt: time.Now(),
2721+
Hex: roundTx.Hex,
26422722
})
26432723
}
26442724
}
@@ -2661,6 +2741,7 @@ func (a *covenantlessArkClient) handleRoundTx(
26612741
Type: types.TxSent,
26622742
Settled: true,
26632743
CreatedAt: time.Now(),
2744+
Hex: roundTx.Hex,
26642745
})
26652746
}
26662747

@@ -2758,6 +2839,7 @@ func (a *covenantlessArkClient) handleRedeemTx(
27582839
Amount: amount,
27592840
Type: types.TxReceived,
27602841
CreatedAt: time.Now(),
2842+
Hex: redeemTx.Hex,
27612843
})
27622844
}
27632845
} else {

0 commit comments

Comments
 (0)