Add GetTransactionsStream RPC (#345)

* RPC GetPaymentsStream

This introduces a new feature to the ArkService API that allows clients to subscribe to payment events. Here's a breakdown of the changes:

1. **OpenAPI Specification (`service.swagger.json`):**
   - A new endpoint `/v1/payments` is added to the API, supporting a `GET` operation for streaming payment events.
   - New definitions `v1GetPaymentsStreamResponse`, `v1RoundPayment`, and `v1AsyncPayment` are added to describe the structure of the streaming responses.

2. **Protobuf Definition (`service.proto`):**
   - Added a new RPC method `GetPaymentsStream` that streams `GetPaymentsStreamResponse` messages.
   - Defined new message types: `GetPaymentsStreamRequest`, `GetPaymentsStreamResponse`, `RoundPayment`, and `AsyncPayment`.

3. **Generated Protobuf Code (`service.pb.go`, `service.pb.gw.go`, `service_grpc.pb.go`):**
   - The generated code is updated to include the new RPC method and message types.
   - The gateway code includes functions to handle HTTP requests and responses for the new streaming endpoint.

4. **Application Logic (`covenant.go`, `covenantless.go`):**
   - New payment events channels are introduced (`paymentEventsCh`).
   - Payment events are propagated to these channels when a round is finalized or an async payment is completed.
   - New event types `RoundPaymentEvent` and `AsyncPaymentEvent` are defined, implementing a `PaymentEvent` interface.

5. **gRPC Handlers (`arkservice.go`):**
   - Added logic to handle `GetPaymentsStream` requests and manage payment listeners.
   - A new goroutine is started to listen to payment events and forward them to active listeners.

Overall, this patch extends the ArkService to support real-time streaming of payment events, allowing clients to receive updates on both round payments and async payments as they occur.

* Move emit events in updateVtxoSet & Use generics and parsers (#1)

* Move sending event to updateVtxoSet

* Use generics and parsers

* pr review refactor

* pr review refactor

* fix

---------

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
This commit is contained in:
Dusan Sekulic
2024-10-04 12:23:16 +02:00
committed by GitHub
parent 26bcbc8163
commit d37af7daf5
10 changed files with 996 additions and 206 deletions

View File

@@ -45,7 +45,8 @@ type covenantService struct {
paymentRequests *paymentsMap
forfeitTxs *forfeitTxsMap
eventsCh chan domain.RoundEvent
eventsCh chan domain.RoundEvent
transactionEventsCh chan TransactionEvent
currentRoundLock sync.Mutex
currentRound *domain.Round
@@ -59,23 +60,30 @@ func NewCovenantService(
builder ports.TxBuilder, scanner ports.BlockchainScanner,
scheduler ports.SchedulerService,
) (Service, error) {
eventsCh := make(chan domain.RoundEvent)
paymentRequests := newPaymentsMap()
forfeitTxs := newForfeitTxsMap(builder)
pubkey, err := walletSvc.GetPubkey(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to fetch pubkey: %s", err)
}
sweeper := newSweeper(walletSvc, repoManager, builder, scheduler)
svc := &covenantService{
network, pubkey,
roundLifetime, roundInterval, unilateralExitDelay, boardingExitDelay,
walletSvc, repoManager, builder, scanner, sweeper,
paymentRequests, forfeitTxs, eventsCh, sync.Mutex{}, nil, nil,
network: network,
pubkey: pubkey,
roundLifetime: roundLifetime,
roundInterval: roundInterval,
unilateralExitDelay: unilateralExitDelay,
boardingExitDelay: boardingExitDelay,
wallet: walletSvc,
repoManager: repoManager,
builder: builder,
scanner: scanner,
sweeper: newSweeper(walletSvc, repoManager, builder, scheduler),
paymentRequests: newPaymentsMap(),
forfeitTxs: newForfeitTxsMap(builder),
eventsCh: make(chan domain.RoundEvent),
transactionEventsCh: make(chan TransactionEvent),
currentRoundLock: sync.Mutex{},
}
repoManager.RegisterEventsHandler(
func(round *domain.Round) {
go svc.propagateEvents(round)
@@ -346,6 +354,10 @@ func (s *covenantService) GetEventsChannel(ctx context.Context) <-chan domain.Ro
return s.eventsCh
}
func (s *covenantService) GetTransactionEventsChannel(ctx context.Context) <-chan TransactionEvent {
return s.transactionEventsCh
}
func (s *covenantService) GetRoundByTxid(ctx context.Context, poolTxid string) (*domain.Round, error) {
return s.repoManager.Rounds().GetRoundWithTxid(ctx, poolTxid)
}
@@ -851,6 +863,26 @@ func (s *covenantService) updateVtxoSet(round *domain.Round) {
}
}()
}
go func() {
// nolint:all
tx, _ := psetv2.NewPsetFromBase64(round.UnsignedTx)
boardingInputs := make([]domain.VtxoKey, 0)
for _, in := range tx.Inputs {
if len(in.TapLeafScript) > 0 {
boardingInputs = append(boardingInputs, domain.VtxoKey{
Txid: elementsutil.TxIDFromBytes(in.PreviousTxid),
VOut: in.PreviousTxIndex,
})
}
}
s.transactionEventsCh <- RoundTransactionEvent{
RoundTxID: round.Txid,
SpentVtxos: getSpentVtxos(round.Payments),
SpendableVtxos: s.getNewVtxos(round),
ClaimedBoardingInputs: boardingInputs,
}
}()
}
func (s *covenantService) propagateEvents(round *domain.Round) {

View File

@@ -42,7 +42,8 @@ type covenantlessService struct {
paymentRequests *paymentsMap
forfeitTxs *forfeitTxsMap
eventsCh chan domain.RoundEvent
eventsCh chan domain.RoundEvent
transactionEventsCh chan TransactionEvent
// cached data for the current round
lastEvent domain.RoundEvent
@@ -62,16 +63,11 @@ func NewCovenantlessService(
builder ports.TxBuilder, scanner ports.BlockchainScanner,
scheduler ports.SchedulerService,
) (Service, error) {
eventsCh := make(chan domain.RoundEvent)
paymentRequests := newPaymentsMap()
forfeitTxs := newForfeitTxsMap(builder)
pubkey, err := walletSvc.GetPubkey(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to fetch pubkey: %s", err)
}
sweeper := newSweeper(walletSvc, repoManager, builder, scheduler)
asyncPaymentsCache := make(map[string]struct {
receivers []domain.Receiver
expireAt int64
@@ -87,10 +83,11 @@ func NewCovenantlessService(
repoManager: repoManager,
builder: builder,
scanner: scanner,
sweeper: sweeper,
paymentRequests: paymentRequests,
forfeitTxs: forfeitTxs,
eventsCh: eventsCh,
sweeper: newSweeper(walletSvc, repoManager, builder, scheduler),
paymentRequests: newPaymentsMap(),
forfeitTxs: newForfeitTxsMap(builder),
eventsCh: make(chan domain.RoundEvent),
transactionEventsCh: make(chan TransactionEvent),
currentRoundLock: sync.Mutex{},
asyncPaymentsCache: asyncPaymentsCache,
treeSigningSessions: make(map[string]*musigSigningSession),
@@ -303,6 +300,14 @@ func (s *covenantlessService) CompleteAsyncPayment(
delete(s.asyncPaymentsCache, redeemTxid)
go func() {
s.transactionEventsCh <- RedeemTransactionEvent{
AsyncTxID: redeemTxid,
SpentVtxos: spentVtxos,
SpendableVtxos: vtxos,
}
}()
return nil
}
@@ -579,6 +584,10 @@ func (s *covenantlessService) GetEventsChannel(ctx context.Context) <-chan domai
return s.eventsCh
}
func (s *covenantlessService) GetTransactionEventsChannel(ctx context.Context) <-chan TransactionEvent {
return s.transactionEventsCh
}
func (s *covenantlessService) GetRoundByTxid(ctx context.Context, roundTxid string) (*domain.Round, error) {
return s.repoManager.Rounds().GetRoundWithTxid(ctx, roundTxid)
}
@@ -1262,7 +1271,27 @@ func (s *covenantlessService) updateVtxoSet(round *domain.Round) {
return
}
}()
}
go func() {
// nolint:all
tx, _ := psbt.NewFromRawBytes(strings.NewReader(round.UnsignedTx), true)
boardingInputs := make([]domain.VtxoKey, 0)
for i, in := range tx.Inputs {
if len(in.TaprootLeafScript) > 0 {
boardingInputs = append(boardingInputs, domain.VtxoKey{
Txid: tx.UnsignedTx.TxIn[i].PreviousOutPoint.Hash.String(),
VOut: tx.UnsignedTx.TxIn[i].PreviousOutPoint.Index,
})
}
}
s.transactionEventsCh <- RoundTransactionEvent{
RoundTxID: round.Txid,
SpentVtxos: getSpentVtxos(round.Payments),
SpendableVtxos: s.getNewVtxos(round),
ClaimedBoardingInputs: boardingInputs,
}
}()
}
func (s *covenantlessService) propagateEvents(round *domain.Round) {

View File

@@ -50,6 +50,7 @@ type Service interface {
ctx context.Context, roundID string,
pubkey *secp256k1.PublicKey, signatures string,
) error
GetTransactionEventsChannel(ctx context.Context) <-chan TransactionEvent
}
type ServiceInfo struct {
@@ -81,3 +82,35 @@ func (outpoint txOutpoint) GetTxid() string {
func (outpoint txOutpoint) GetIndex() uint32 {
return outpoint.vout
}
const (
RoundTransaction TransactionEventType = "round_tx"
RedeemTransaction TransactionEventType = "redeem_tx"
)
type TransactionEventType string
type TransactionEvent interface {
Type() TransactionEventType
}
type RoundTransactionEvent struct {
RoundTxID string
SpentVtxos []domain.VtxoKey
SpendableVtxos []domain.Vtxo
ClaimedBoardingInputs []domain.VtxoKey
}
func (r RoundTransactionEvent) Type() TransactionEventType {
return RoundTransaction
}
type RedeemTransactionEvent struct {
AsyncTxID string
SpentVtxos []domain.VtxoKey
SpendableVtxos []domain.Vtxo
}
func (a RedeemTransactionEvent) Type() TransactionEventType {
return RedeemTransaction
}