Skip to content

Commit c5eea15

Browse files
feat(SPV-1535) handle ARC callback (#935)
Co-authored-by: Damian Orzepowski <damian.orzepowski@4chain.studio>
1 parent cf00a42 commit c5eea15

40 files changed

+1164
-55
lines changed

actions/testabilities/assert_spvwallet_application.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type SPVWalletApplicationAssertions interface {
2121
Response(response *resty.Response) SPVWalletResponseAssertions
2222
User(user fixtures.User) SPVWalletAppUserAssertions
2323
ExternalPaymailHost() testpaymail.PaymailExternalAssertions
24+
ARC() testengine.ARCAssertions
2425
}
2526

2627
type SPVWalletResponseAssertions interface {
@@ -80,6 +81,10 @@ func (a *appAssertions) ExternalPaymailHost() testpaymail.PaymailExternalAsserti
8081
return a.engineAssertions.ExternalPaymailHost()
8182
}
8283

84+
func (a *appAssertions) ARC() testengine.ARCAssertions {
85+
return a.engineAssertions.ARC()
86+
}
87+
8388
type responseAssertions struct {
8489
t testing.TB
8590
require *require.Assertions

actions/testabilities/assert_spvwallet_user.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ type LastOperationAssertions interface {
3131
WithCounterparty(counterparty string) LastOperationAssertions
3232
WithNoCounterparty() LastOperationAssertions
3333
WithTxStatus(txStatus string) LastOperationAssertions
34+
WithBlockHeight(blockHeight int64) LastOperationAssertions
35+
WithBlockHash(blockHash string) LastOperationAssertions
3436
}
3537

3638
type userAssertions struct {
@@ -131,3 +133,17 @@ func (l *lastOperationAssertions) WithTxStatus(txStatus string) LastOperationAss
131133
l.require.EqualValues(txStatus, l.content.TxStatus)
132134
return l
133135
}
136+
137+
func (l *lastOperationAssertions) WithBlockHeight(blockHeight int64) LastOperationAssertions {
138+
l.t.Helper()
139+
l.require.NotNil(l.content.BlockHeight)
140+
l.require.EqualValues(blockHeight, *l.content.BlockHeight)
141+
return l
142+
}
143+
144+
func (l *lastOperationAssertions) WithBlockHash(blockHash string) LastOperationAssertions {
145+
l.t.Helper()
146+
l.require.NotNil(l.content.BlockHash)
147+
l.require.Equal(blockHash, *l.content.BlockHash)
148+
return l
149+
}

actions/testabilities/fixture_spvwallet_application.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type SPVWalletApplicationFixture interface {
4242

4343
// Tx creates a new mocked transaction builder
4444
Tx() txtestability.TransactionSpec
45+
46+
Config() *config.AppConfig
4547
}
4648

4749
type BlockHeadersServiceFixture interface {
@@ -167,3 +169,7 @@ func (f *appFixture) Tx() txtestability.TransactionSpec {
167169
func (f *appFixture) EngineFixture() testengine.EngineFixture {
168170
return f.engineFixture
169171
}
172+
173+
func (f *appFixture) Config() *config.AppConfig {
174+
return &f.engineWithConfig.Config
175+
}

actions/transactions/broadcast_callback.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,23 @@ import (
1212
// broadcastCallback will handle a broadcastCallback call from the broadcast api
1313
func broadcastCallback(c *gin.Context) {
1414
logger := reqctx.Logger(c)
15-
var callbackResp *chainmodels.TXInfo
15+
config := reqctx.AppConfig(c)
16+
var callbackResp chainmodels.TXInfo
1617

1718
err := c.Bind(&callbackResp)
1819
if err != nil {
1920
spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest, logger)
2021
return
2122
}
2223

23-
err = reqctx.Engine(c).HandleTxCallback(c.Request.Context(), callbackResp)
24+
if config.ExperimentalFeatures.V2 {
25+
err = reqctx.Engine(c).TxSyncService().Handle(c, callbackResp)
26+
} else {
27+
err = reqctx.Engine(c).HandleTxCallback(c, &callbackResp)
28+
}
29+
2430
if err != nil {
25-
logger.Err(err).Msgf("failed to update transaction - tx: %v", callbackResp)
31+
logger.Err(err).Any("TxInfo", callbackResp).Msgf("failed to update transaction in ARC broadcast callback handler")
2632
}
2733

2834
c.Status(http.StatusOK)

actions/transactions/routes.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package transactions
22

33
import (
4-
"github.com/bitcoin-sv/spv-wallet/config"
4+
"fmt"
5+
56
"github.com/bitcoin-sv/spv-wallet/server/handlers"
67
)
78

@@ -15,5 +16,16 @@ func RegisterRoutes(handlersManager *handlers.Manager) {
1516
group.POST("/drafts", handlers.AsUser(newTransactionDraft))
1617
group.POST("", handlers.AsUser(recordTransaction))
1718

18-
handlersManager.Get(handlers.GroupTransactionCallback).POST(config.BroadcastCallbackRoute, broadcastCallback)
19+
registerARCCallback(handlersManager)
20+
}
21+
22+
func registerARCCallback(handlersManager *handlers.Manager) {
23+
config := handlersManager.GetConfig()
24+
if config.ARCCallbackEnabled() {
25+
callbackURL, err := config.ARC.Callback.ShouldGetURL()
26+
if err != nil {
27+
panic(fmt.Sprintf(`couldn't get callback URL from configuration: %v`, err.Error()))
28+
}
29+
handlersManager.Get(handlers.GroupTransactionCallback).POST(callbackURL.Path, broadcastCallback)
30+
}
1931
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package integrationtests
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/bitcoin-sv/go-sdk/chainhash"
8+
trx "github.com/bitcoin-sv/go-sdk/transaction"
9+
"github.com/bitcoin-sv/spv-wallet/actions/v2/internal/integrationtests/testabilities"
10+
chainmodels "github.com/bitcoin-sv/spv-wallet/engine/chain/models"
11+
testengine "github.com/bitcoin-sv/spv-wallet/engine/testabilities"
12+
"github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/txmodels"
13+
)
14+
15+
func TestHandlingARCCallback(t *testing.T) {
16+
tests := map[string]struct {
17+
txInfo chainmodels.TXInfo
18+
expectStatus txmodels.TxStatus
19+
beforeCallback func(t *testing.T, txInfo *chainmodels.TXInfo)
20+
}{
21+
"On Mined with BUMP": {
22+
txInfo: chainmodels.TXInfo{
23+
TXStatus: chainmodels.Mined,
24+
BlockHeight: 885803,
25+
BlockHash: "00000000000000000f0905597b6cac80031f0f56834e74dce1a714c682a9ed38",
26+
Timestamp: time.Now().Add(10 * time.Minute),
27+
},
28+
beforeCallback: calcBump,
29+
expectStatus: txmodels.TxStatusMined,
30+
},
31+
"On SeenOnNetwork do nothing": {
32+
txInfo: minimalTxInfo(chainmodels.SeenOnNetwork),
33+
expectStatus: txmodels.TxStatusBroadcasted,
34+
},
35+
"On Rejected mark as problematic": {
36+
txInfo: minimalTxInfo(chainmodels.Rejected),
37+
expectStatus: txmodels.TxStatusProblematic,
38+
},
39+
}
40+
for name, test := range tests {
41+
t.Run(name, func(t *testing.T) {
42+
// given:
43+
given, when, then := testabilities.New(t)
44+
cleanup := given.StartedSPVWalletV2()
45+
defer cleanup()
46+
47+
// when:
48+
receiveTxID := when.Alice().ReceivesFromExternal(10)
49+
50+
// then:
51+
then.ARC().Broadcasted().
52+
WithTxID(receiveTxID).
53+
WithCallbackURL("https://example.com/transaction/broadcast/callback").
54+
WithCallbackToken(testengine.CallbackTestToken)
55+
56+
// and:
57+
then.Alice().Operations().Last().
58+
WithTxID(receiveTxID).
59+
WithTxStatus("BROADCASTED")
60+
61+
// given:
62+
test.txInfo.TxID = receiveTxID
63+
if test.beforeCallback != nil {
64+
test.beforeCallback(t, &test.txInfo)
65+
}
66+
67+
// when:
68+
test.txInfo.TxID = receiveTxID
69+
if test.beforeCallback != nil {
70+
test.beforeCallback(t, &test.txInfo)
71+
}
72+
when.ARC().SendsCallback(test.txInfo)
73+
74+
// then:
75+
then.Alice().Operations().Last().
76+
WithTxID(receiveTxID).
77+
WithTxStatus(string(test.expectStatus))
78+
79+
if test.expectStatus == txmodels.TxStatusMined {
80+
then.Alice().Operations().Last().
81+
WithBlockHash(test.txInfo.BlockHash).
82+
WithBlockHeight(test.txInfo.BlockHeight)
83+
}
84+
85+
// TODO: Assert for BEEF after Searching Transactions is implemented
86+
})
87+
}
88+
}
89+
90+
func minimalTxInfo(txStatus chainmodels.TXStatus) chainmodels.TXInfo {
91+
return chainmodels.TXInfo{
92+
TXStatus: txStatus,
93+
Timestamp: time.Now().Add(10 * time.Minute),
94+
}
95+
}
96+
97+
func calcBump(t *testing.T, txInfo *chainmodels.TXInfo) {
98+
t.Helper()
99+
100+
txID, _ := chainhash.NewHashFromHex(txInfo.TxID)
101+
bump := trx.NewMerklePath(uint32(txInfo.BlockHeight), [][]*trx.PathElement{{
102+
&trx.PathElement{
103+
Hash: txID,
104+
Offset: 0,
105+
},
106+
}})
107+
txInfo.MerklePath = bump.Hex()
108+
}

actions/v2/internal/integrationtests/spend_external_funds_internally_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func TestSpendExternalFundsInternally(t *testing.T) {
1212
cleanup := given.StartedSPVWalletV2()
1313
defer cleanup()
1414

15-
// and:
15+
// when:
1616
receiveTxID := when.Alice().ReceivesFromExternal(10)
1717

1818
// then:

actions/v2/internal/integrationtests/testabilities/ability_actions.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type IntegrationTestAction interface {
3636
Alice() ActorsActions
3737
Bob() ActorsActions
3838
Charlie() ActorsActions
39+
ARC() ARCActions
3940
}
4041

4142
type ActorsActions interface {
@@ -46,6 +47,10 @@ type ActorsActions interface {
4647
CreatesOutline() OutlineBuilder
4748
}
4849

50+
type ARCActions interface {
51+
SendsCallback(txInfo chainmodels.TXInfo)
52+
}
53+
4954
type actions struct {
5055
t testing.TB
5156
fixture *fixture
@@ -62,6 +67,13 @@ func (a *actions) Alice() ActorsActions {
6267
return a.fixture.alice
6368
}
6469

70+
func (a *actions) ARC() ARCActions {
71+
return &arcActions{
72+
t: a.t,
73+
fixture: a.fixture,
74+
}
75+
}
76+
6577
func (a *actions) Bob() ActorsActions {
6678
return a.fixture.bob
6779
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package testabilities
2+
3+
import (
4+
"testing"
5+
6+
chainmodels "github.com/bitcoin-sv/spv-wallet/engine/chain/models"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
type arcActions struct {
11+
t testing.TB
12+
fixture *fixture
13+
}
14+
15+
func (a *arcActions) SendsCallback(txInfo chainmodels.TXInfo) {
16+
client := a.fixture.HttpClient().ForAnonymous()
17+
token := a.fixture.Config().ARC.Callback.Token
18+
19+
res, _ := client.R().
20+
SetHeader("Content-Type", "application/json").
21+
SetBody(txInfo).
22+
SetAuthToken(token).
23+
Post("/transaction/broadcast/callback")
24+
25+
require.Equal(a.t, 200, res.StatusCode())
26+
}

actions/v2/internal/integrationtests/testabilities/assert_actors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"testing"
55

66
"github.com/bitcoin-sv/spv-wallet/actions/testabilities"
7+
testengine "github.com/bitcoin-sv/spv-wallet/engine/testabilities"
78
)
89

910
type IntegrationTestAssertion interface {
1011
Alice() SpvWalletActorsStateAssertions
1112
Bob() SpvWalletActorsStateAssertions
1213
Charlie() SpvWalletActorsStateAssertions
14+
ARC() testengine.ARCAssertions
1315
}
1416

1517
// SpvWalletActorsStateAssertions about spv-wallet users

0 commit comments

Comments
 (0)