diff --git a/actions/paymailserver/incoming_paymail_tx_test.go b/actions/paymailserver/incoming_paymail_tx_test.go new file mode 100644 index 000000000..8e4578a78 --- /dev/null +++ b/actions/paymailserver/incoming_paymail_tx_test.go @@ -0,0 +1,229 @@ +package paymailserver_test + +import ( + "fmt" + "testing" + + "github.com/bitcoin-sv/go-sdk/script" + "github.com/bitcoin-sv/spv-wallet/actions/testabilities" + chainmodels "github.com/bitcoin-sv/spv-wallet/engine/chain/models" + testengine "github.com/bitcoin-sv/spv-wallet/engine/testabilities" + "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" + "github.com/stretchr/testify/require" +) + +func TestIncomingPaymailRawTX(t *testing.T) { + givenForAllTests := testabilities.Given(t) + cleanup := givenForAllTests.StartedSPVWalletWithConfiguration( + testengine.WithDomainValidationDisabled(), + ) + defer cleanup() + + var testState struct { + reference string + lockingScript *script.Script + } + + // given: + given, then := testabilities.NewOf(givenForAllTests, t) + client := given.HttpClient().ForAnonymous() + + // and: + address := fixtures.Sender.Paymails[0] + satoshis := uint64(1000) + note := "test note" + + t.Run("step 1 - call p2p-payment-destination", func(t *testing.T) { + // given: + requestBody := map[string]any{ + "satoshis": satoshis, + } + + // when: + res, _ := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(requestBody). + Post( + fmt.Sprintf( + "https://example.com/v1/bsvalias/p2p-payment-destination/%s", + address, + ), + ) + + // then: + then.Response(res).IsOK().WithJSONMatching(`{ + "outputs": [ + { + "address": "{{ matchAddress }}", + "satoshis": {{ .satoshis }}, + "script": "{{ matchHex }}" + } + ], + "reference": "{{ matchHexWithLength 32 }}" + }`, map[string]any{ + "satoshis": satoshis, + }) + + // update: + getter := then.Response(res).JSONValue() + testState.reference = getter.GetString("reference") + + // and: + lockingScript, err := script.NewFromHex(getter.GetString("outputs[0]/script")) + require.NoError(t, err) + testState.lockingScript = lockingScript + }) + + t.Run("step 2 - call receive-transaction capability", func(t *testing.T) { + // given: + txSpec := fixtures.GivenTX(t). + WithInput(satoshis+1). + WithOutputScript(satoshis, testState.lockingScript) + + // and: + requestBody := map[string]any{ + "hex": txSpec.RawTX(), + "reference": testState.reference, + "metadata": map[string]any{ + "note": note, + }, + } + + // and: + given.ARC().WillRespondForBroadcast(200, &chainmodels.TXInfo{ + TxID: txSpec.ID(), + TXStatus: chainmodels.SeenOnNetwork, + }) + + // when: + res, _ := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(requestBody). + Post( + fmt.Sprintf( + "https://example.com/v1/bsvalias/receive-transaction/%s", + address, + ), + ) + + // then: + then.Response(res).IsOK().WithJSONMatching(`{ + "txid": "{{ .txid }}", + "note": "{{ .note }}" + }`, map[string]any{ + "txid": txSpec.ID(), + "note": note, + }) + }) +} + +func TestIncomingPaymailBeef(t *testing.T) { + givenForAllTests := testabilities.Given(t) + cleanup := givenForAllTests.StartedSPVWalletWithConfiguration( + testengine.WithDomainValidationDisabled(), + ) + defer cleanup() + + var testState struct { + reference string + lockingScript *script.Script + txID string + } + + // given: + given, then := testabilities.NewOf(givenForAllTests, t) + client := given.HttpClient().ForAnonymous() + + // and: + address := fixtures.Sender.Paymails[0] + satoshis := uint64(1000) + note := "test note" + + t.Run("step 1 - call p2p-payment-destination", func(t *testing.T) { + // given: + requestBody := map[string]any{ + "satoshis": satoshis, + } + + // when: + res, _ := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(requestBody). + Post( + fmt.Sprintf( + "https://example.com/v1/bsvalias/p2p-payment-destination/%s", + address, + ), + ) + + // then: + then.Response(res).IsOK().WithJSONMatching(`{ + "outputs": [ + { + "address": "{{ matchAddress }}", + "satoshis": {{ .satoshis }}, + "script": "{{ matchHex }}" + } + ], + "reference": "{{ matchHexWithLength 32 }}" + }`, map[string]any{ + "satoshis": satoshis, + }) + + // update: + getter := then.Response(res).JSONValue() + testState.reference = getter.GetString("reference") + + // and: + lockingScript, err := script.NewFromHex(getter.GetString("outputs[0]/script")) + require.NoError(t, err) + testState.lockingScript = lockingScript + }) + + t.Run("step 2 - call beef capability", func(t *testing.T) { + // given: + txSpec := fixtures.GivenTX(t). + WithInput(satoshis+1). + WithOutputScript(satoshis, testState.lockingScript) + + // and: + requestBody := map[string]any{ + "beef": txSpec.BEEF(), + "reference": testState.reference, + "metadata": map[string]any{ + "note": note, + }, + } + + // and: + given.ARC().WillRespondForBroadcast(200, &chainmodels.TXInfo{ + TxID: txSpec.ID(), + TXStatus: chainmodels.SeenOnNetwork, + }) + + // and; + given.BHS().WillRespondForMerkleRootsVerify(200, &chainmodels.MerkleRootsConfirmations{ + ConfirmationState: chainmodels.MRConfirmed, + }) + + // when: + res, _ := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(requestBody). + Post( + fmt.Sprintf( + "https://example.com/v1/bsvalias/beef/%s", + address, + ), + ) + + // then: + then.Response(res).IsOK().WithJSONMatching(`{ + "txid": "{{ .txid }}", + "note": "{{ .note }}" + }`, map[string]any{ + "txid": txSpec.ID(), + "note": note, + }) + }) +} diff --git a/actions/paymailserver/register.go b/actions/paymailserver/register.go new file mode 100644 index 000000000..fc9bd41d0 --- /dev/null +++ b/actions/paymailserver/register.go @@ -0,0 +1,11 @@ +package paymailserver + +import ( + "github.com/bitcoin-sv/go-paymail/server" + "github.com/gin-gonic/gin" +) + +// Register registers the paymail server. +func Register(configuration *server.Configuration, ginEngine *gin.Engine) { + configuration.RegisterRoutes(ginEngine) +} diff --git a/actions/paymailserver/stateless_capabilities_test.go b/actions/paymailserver/stateless_capabilities_test.go new file mode 100644 index 000000000..ad51a44ee --- /dev/null +++ b/actions/paymailserver/stateless_capabilities_test.go @@ -0,0 +1,194 @@ +package paymailserver_test + +import ( + "fmt" + "testing" + + "github.com/bitcoin-sv/spv-wallet/actions/testabilities" + testengine "github.com/bitcoin-sv/spv-wallet/engine/testabilities" + "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" +) + +func TestStatelessCapabilities(t *testing.T) { + givenForAllTests := testabilities.Given(t) + cleanup := givenForAllTests.StartedSPVWalletWithConfiguration( + testengine.WithDomainValidationDisabled(), + ) + defer cleanup() + + t.Run("Get bsv alias", func(t *testing.T) { + // given: + given, then := testabilities.NewOf(givenForAllTests, t) + client := given.HttpClient().ForAnonymous() + + // when: + // NOTE: Because testabilities' client has substituted transport, + // we can make request which looks like it's going to the real server, but instead it goes to the spv-wallet test server. + // Defining the host in the request is necessary for paymail to work. + res, _ := client.R().Get("https://example.com/.well-known/bsvalias") + + // then: + then.Response(res).IsOK().WithJSONf(`{ + "bsvalias": "1.0", + "capabilities": { + "2a40af698840": "https://example.com/v1/bsvalias/p2p-payment-destination/{alias}@{domain.tld}", + "5c55a7fdb7bb": "https://example.com/v1/bsvalias/beef/{alias}@{domain.tld}", + "5f1323cddf31": "https://example.com/v1/bsvalias/receive-transaction/{alias}@{domain.tld}", + "6745385c3fc0": false, + "a9f510c16bde": "https://example.com/v1/bsvalias/verify-pubkey/{alias}@{domain.tld}/{pubkey}", + "f12f968c92d6": "https://example.com/v1/bsvalias/public-profile/{alias}@{domain.tld}", + "paymentDestination": "https://example.com/v1/bsvalias/address/{alias}@{domain.tld}", + "pki": "https://example.com/v1/bsvalias/id/{alias}@{domain.tld}" + } + }`) + }) + + t.Run("Public profile", func(t *testing.T) { + // given: + given, then := testabilities.NewOf(givenForAllTests, t) + client := given.HttpClient().ForAnonymous() + + // and: + address := fixtures.Sender.Paymails[0] + + // when: + res, _ := client.R().Get( + fmt.Sprintf( + "https://example.com/v1/bsvalias/public-profile/%s", + address, + ), + ) + + // then: + then.Response(res).IsOK().WithJSONMatching(`{ + "avatar": "{{ matchURL | orEmpty }}", + "name": "{{ .name }}" + }`, map[string]any{ + "name": address, + }) + }) + + t.Run("Public profile for not existing paymail", func(t *testing.T) { + // given: + given, then := testabilities.NewOf(givenForAllTests, t) + client := given.HttpClient().ForAnonymous() + + // when: + res, _ := client.R().Get("https://example.com/v1/bsvalias/public-profile/notexisting@example.com") + + // then: + then.Response(res).HasStatus(404) + }) + + t.Run("Get PKI and verify", func(t *testing.T) { + // given: + given, then := testabilities.NewOf(givenForAllTests, t) + client := given.HttpClient().ForAnonymous() + + // and: + address := fixtures.Sender.Paymails[0] + + // when: + res, _ := client.R().Get( + fmt.Sprintf( + "https://example.com/v1/bsvalias/id/%s", + address, + ), + ) + + // then: + then.Response(res).IsOK().WithJSONMatching(`{ + "bsvalias": "1.0", + "handle": "{{ .paymail }}", + "pubkey": "{{ matchHexWithLength 66 }}" + }`, map[string]any{ + "paymail": address, + }) + + // given: + pki := then.Response(res).JSONValue().GetString("pubkey") + + // when: + res, _ = client.R().Get( + fmt.Sprintf( + "https://example.com/v1/bsvalias/verify-pubkey/%s/%s", + address, + pki, + ), + ) + + // then: + then.Response(res).IsOK().WithJSONMatching(`{ + "bsvalias": "1.0", + "handle": "{{ .paymail }}", + "match": true, + "pubkey": "{{ .pki }}" + }`, map[string]any{ + "paymail": address, + "pki": pki, + }) + }) + + t.Run("PKI for not existing paymail", func(t *testing.T) { + // given: + given, then := testabilities.NewOf(givenForAllTests, t) + client := given.HttpClient().ForAnonymous() + + // when: + res, _ := client.R().Get("https://example.com/v1/bsvalias/id/notexisting@example") + + // then: + then.Response(res).HasStatus(404) + }) + + t.Run("PubKey verify on wrong key", func(t *testing.T) { + // given: + given, then := testabilities.NewOf(givenForAllTests, t) + client := given.HttpClient().ForAnonymous() + + // and: + address := fixtures.Sender.Paymails[0] + wrongPKI := "02561fc133e140526f11438550de3e6cf0ae246a4a5bcd151230652b60124ea1d9" + + // when: + res, _ := client.R().Get( + fmt.Sprintf( + "https://example.com/v1/bsvalias/verify-pubkey/%s/%s", + address, + wrongPKI, + ), + ) + + // then: + then.Response(res).IsOK().WithJSONMatching(`{ + "bsvalias": "1.0", + "handle": "{{ .paymail }}", + "match": false, + "pubkey": "{{ matchHexWithLength 66 }}" + }`, map[string]any{ + "paymail": address, + }) + }) + + t.Run("PubKey verify on not existing paymail", func(t *testing.T) { + // given: + given, then := testabilities.NewOf(givenForAllTests, t) + client := given.HttpClient().ForAnonymous() + + // and: + notExistingPaymail := "notexisting@example.com" + wrongPKI := "02561fc133e140526f11438550de3e6cf0ae246a4a5bcd151230652b60124ea1d9" + + // when: + res, _ := client.R().Get( + fmt.Sprintf( + "https://example.com/v1/bsvalias/verify-pubkey/%s/%s", + notExistingPaymail, + wrongPKI, + ), + ) + + // then: + then.Response(res).HasStatus(404) + }) +} diff --git a/actions/testabilities/assert_spvwallet_application.go b/actions/testabilities/assert_spvwallet_application.go index 37d73598a..d1562e569 100644 --- a/actions/testabilities/assert_spvwallet_application.go +++ b/actions/testabilities/assert_spvwallet_application.go @@ -23,6 +23,7 @@ type SPVWalletResponseAssertions interface { HasStatus(status int) SPVWalletResponseAssertions WithJSONf(expectedFormat string, args ...any) WithJSONMatching(expectedTemplateFormat string, params map[string]any) + JSONValue() JsonValueGetter // IsUnauthorized asserts that the response status code is 401 and the error is about lack of authorization. IsUnauthorized() // IsUnauthorizedForAdmin asserts that the response status code is 401 and the error is that admin is not authorized to use the endpoint. @@ -32,6 +33,10 @@ type SPVWalletResponseAssertions interface { IsBadRequest() SPVWalletResponseAssertions } +type JsonValueGetter interface { + GetString(xpath string) string +} + func Then(t testing.TB) SPVWalletApplicationAssertions { return &responseAssertions{ t: t, @@ -92,6 +97,11 @@ func (a *responseAssertions) WithJSONMatching(expectedTemplateFormat string, par jsonrequire.Match(a.t, expectedTemplateFormat, params, a.response.String()) } +func (a *responseAssertions) JSONValue() JsonValueGetter { + a.assertJSONContentType() + return jsonrequire.NewGetterWithJSON(a.t, a.response.String()) +} + func (a *responseAssertions) assertJSONContentType() { contentType := a.response.Header().Get("Content-Type") mediaType, _, err := mime.ParseMediaType(contentType) diff --git a/actions/testabilities/fixture_spvwallet_application.go b/actions/testabilities/fixture_spvwallet_application.go index 82ecce4e2..360d2721c 100644 --- a/actions/testabilities/fixture_spvwallet_application.go +++ b/actions/testabilities/fixture_spvwallet_application.go @@ -37,6 +37,10 @@ type BlockHeadersServiceFixture interface { // WillRespondForMerkleRoots returns a http response for get merkleroots endpoint with // provided httpCode and response WillRespondForMerkleRoots(httpCode int, response string) + + // WillRespondForMerkleRootsVerify returns a MerkleRootsConfirmations response for get merkleroot/verify endpoint with + // provided httpCode + WillRespondForMerkleRootsVerify(httpCode int, response *chainmodels.MerkleRootsConfirmations) } type ARCFixture interface { diff --git a/engine/testabilities/fixture_block_header_service.go b/engine/testabilities/fixture_block_header_service.go index 03bebaee1..e496325f7 100644 --- a/engine/testabilities/fixture_block_header_service.go +++ b/engine/testabilities/fixture_block_header_service.go @@ -2,6 +2,7 @@ package testabilities import ( "encoding/json" + chainmodels "github.com/bitcoin-sv/spv-wallet/engine/chain/models" "net/http" "slices" @@ -15,6 +16,10 @@ type BlockHeadersServiceFixture interface { // WillRespondForMerkleRoots returns a http response for get merkleroots endpoint with // provided httpCode and response WillRespondForMerkleRoots(httpCode int, response string) + + // WillRespondForMerkleRootsVerify returns a MerkleRootsConfirmations response for get merkleroot/verify endpoint with + // provided httpCode + WillRespondForMerkleRootsVerify(httpCode int, response *chainmodels.MerkleRootsConfirmations) } func (f *engineFixture) BHS() BlockHeadersServiceFixture { @@ -32,6 +37,17 @@ func (f *engineFixture) WillRespondForMerkleRoots(httpCode int, response string) f.externalTransport.RegisterResponder("GET", "http://localhost:8080/api/v1/chain/merkleroot", responder) } +func (f *engineFixture) WillRespondForMerkleRootsVerify(httpCode int, response *chainmodels.MerkleRootsConfirmations) { + responder := func(req *http.Request) (*http.Response, error) { + if response == nil { + return httpmock.NewStringResponse(httpCode, ""), nil + } + return httpmock.NewJsonResponse(httpCode, response) + } + + f.externalTransport.RegisterResponder("POST", "http://localhost:8080/api/v1/chain/merkleroot/verify", responder) +} + func (f *engineFixture) mockBHSGetMerkleRoots() { responder := func(req *http.Request) (*http.Response, error) { if req.Header.Get("Authorization") != "Bearer "+f.config.BHS.AuthToken { diff --git a/engine/testabilities/fixture_configopts.go b/engine/testabilities/fixture_configopts.go index d75487bca..08e2c0366 100644 --- a/engine/testabilities/fixture_configopts.go +++ b/engine/testabilities/fixture_configopts.go @@ -10,6 +10,12 @@ func WithNewTransactionFlowEnabled() ConfigOpts { } } +func WithDomainValidationDisabled() ConfigOpts { + return func(c *config.AppConfig) { + c.Paymail.DomainValidationEnabled = false + } +} + func WithNotificationsEnabled() ConfigOpts { return func(c *config.AppConfig) { c.Notifications.Enabled = true diff --git a/engine/tester/fixtures/tx_fixtures.go b/engine/tester/fixtures/tx_fixtures.go index 59e0e41d7..a89c97a80 100644 --- a/engine/tester/fixtures/tx_fixtures.go +++ b/engine/tester/fixtures/tx_fixtures.go @@ -34,7 +34,8 @@ type GivenTXSpec interface { WithInput(satoshis uint64) GivenTXSpec WithSingleSourceInputs(satoshis ...uint64) GivenTXSpec WithOPReturn(dataStr string) GivenTXSpec - WithOutputScript(parts ...ScriptPart) GivenTXSpec + WithOutputScriptParts(parts ...ScriptPart) GivenTXSpec + WithOutputScript(satoshis uint64, script *script.Script) GivenTXSpec WithP2PKHOutput(satoshis uint64) GivenTXSpec TX() *trx.Transaction @@ -117,6 +118,15 @@ func (spec *txSpec) WithP2PKHOutput(satoshis uint64) GivenTXSpec { return spec } +// WithOutputScript adds an output to the transaction with the specified satoshis and script +func (spec *txSpec) WithOutputScript(satoshis uint64, script *script.Script) GivenTXSpec { + spec.outputs = append(spec.outputs, &trx.TransactionOutput{ + Satoshis: satoshis, + LockingScript: script, + }) + return spec +} + // ScriptPart is an interface for building script parts type ScriptPart interface { Append(s *script.Script) error @@ -138,8 +148,8 @@ func (data PushData) Append(s *script.Script) error { return s.AppendPushData(data) } -// WithOutputScript adds an output to the transaction with the specified script parts -func (spec *txSpec) WithOutputScript(parts ...ScriptPart) GivenTXSpec { +// WithOutputScriptParts adds an output to the transaction with the specified script parts +func (spec *txSpec) WithOutputScriptParts(parts ...ScriptPart) GivenTXSpec { s := &script.Script{} for _, part := range parts { err := part.Append(s) @@ -242,9 +252,8 @@ func (spec *txSpec) makeParentTX(satoshis ...uint64) *trx.Transaction { // each merkle proof should have a different block height to not collide with each other err = tx.AddMerkleProof(trx.NewMerklePath(spec.getNextBlockHeight(), [][]*trx.PathElement{{ &trx.PathElement{ - Hash: tx.TxID(), - Offset: 0, - Duplicate: ptr(true), + Hash: tx.TxID(), + Offset: 0, }, }})) require.NoError(spec.t, err, "adding merkle proof to parent tx") diff --git a/engine/tester/fixtures/tx_fixtures_test.go b/engine/tester/fixtures/tx_fixtures_test.go index 2c99f6175..3c9908111 100644 --- a/engine/tester/fixtures/tx_fixtures_test.go +++ b/engine/tester/fixtures/tx_fixtures_test.go @@ -13,7 +13,7 @@ func givenTXSpec(t *testing.T) GivenTXSpec { WithSingleSourceInputs(2, 3, 4). WithP2PKHOutput(1). WithOPReturn("hello world"). - WithOutputScript(OpCode(script.OpRETURN), PushData("hello world")) + WithOutputScriptParts(OpCode(script.OpRETURN), PushData("hello world")) } /* @@ -38,7 +38,7 @@ func TestMockTXGeneration(t *testing.T) { }, "signed complex tx": { spec: givenTXSpec(t), - beef: "0100beef01fde803010100010201000000012e3f4683e173b40a20527fe5719633ba070df649983614886e90e45aecf2ac56000000006a47304402205c05a6c9eadda5da97eddb55a37178e946d4be0b151670233bc76cbebbee011c022018845d47e6fa8dd0100258bf855dc4990ad49c72de65f04b1b85ea146d1f117a4121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff0302000000000000001976a91494677c56fa2968644c90a517214338b4139899ce88ac03000000000000001976a91494677c56fa2968644c90a517214338b4139899ce88ac04000000000000001976a91494677c56fa2968644c90a517214338b4139899ce88ac000000000100010000000318aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7000000006b483045022100f5fb8c86bb12a2cc45c2f62cf590725209362c8da0618010a3009625462cddf302205856c21d1daf1f2758c1d03f6acb2f27e361f998d48d732cb8ab8d494eac8e894121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff18aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7010000006a47304402207877451733ab726fc23288084dd99a6450490a70db06483c1709f7b482610cbc02207b7c84cca5e345a2a3a56fc59ba8ee12cadb856d3032a7aff7c3719c84048e144121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff18aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7020000006b483045022100e500eba499e76490c6936c3ac13e1ed808edcd671d113f5fa76f23951116cbd40220576ff4993ce3ec757949aa0fc2bfe1b541660b520f0d1ed745cf79a210d9e3ff4121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff0301000000000000001976a9143cf53c49c322d9d811728182939aee2dca087f9888ac00000000000000000e006a0b68656c6c6f20776f726c6400000000000000000d6a0b68656c6c6f20776f726c640000000000", + beef: "0100beef01fde8030101000018aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb70201000000012e3f4683e173b40a20527fe5719633ba070df649983614886e90e45aecf2ac56000000006a47304402205c05a6c9eadda5da97eddb55a37178e946d4be0b151670233bc76cbebbee011c022018845d47e6fa8dd0100258bf855dc4990ad49c72de65f04b1b85ea146d1f117a4121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff0302000000000000001976a91494677c56fa2968644c90a517214338b4139899ce88ac03000000000000001976a91494677c56fa2968644c90a517214338b4139899ce88ac04000000000000001976a91494677c56fa2968644c90a517214338b4139899ce88ac000000000100010000000318aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7000000006b483045022100f5fb8c86bb12a2cc45c2f62cf590725209362c8da0618010a3009625462cddf302205856c21d1daf1f2758c1d03f6acb2f27e361f998d48d732cb8ab8d494eac8e894121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff18aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7010000006a47304402207877451733ab726fc23288084dd99a6450490a70db06483c1709f7b482610cbc02207b7c84cca5e345a2a3a56fc59ba8ee12cadb856d3032a7aff7c3719c84048e144121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff18aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7020000006b483045022100e500eba499e76490c6936c3ac13e1ed808edcd671d113f5fa76f23951116cbd40220576ff4993ce3ec757949aa0fc2bfe1b541660b520f0d1ed745cf79a210d9e3ff4121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff0301000000000000001976a9143cf53c49c322d9d811728182939aee2dca087f9888ac00000000000000000e006a0b68656c6c6f20776f726c6400000000000000000d6a0b68656c6c6f20776f726c640000000000", rawTX: "010000000318aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7000000006b483045022100f5fb8c86bb12a2cc45c2f62cf590725209362c8da0618010a3009625462cddf302205856c21d1daf1f2758c1d03f6acb2f27e361f998d48d732cb8ab8d494eac8e894121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff18aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7010000006a47304402207877451733ab726fc23288084dd99a6450490a70db06483c1709f7b482610cbc02207b7c84cca5e345a2a3a56fc59ba8ee12cadb856d3032a7aff7c3719c84048e144121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff18aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7020000006b483045022100e500eba499e76490c6936c3ac13e1ed808edcd671d113f5fa76f23951116cbd40220576ff4993ce3ec757949aa0fc2bfe1b541660b520f0d1ed745cf79a210d9e3ff4121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff0301000000000000001976a9143cf53c49c322d9d811728182939aee2dca087f9888ac00000000000000000e006a0b68656c6c6f20776f726c6400000000000000000d6a0b68656c6c6f20776f726c6400000000", ef: "010000000000000000ef0318aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7000000006b483045022100f5fb8c86bb12a2cc45c2f62cf590725209362c8da0618010a3009625462cddf302205856c21d1daf1f2758c1d03f6acb2f27e361f998d48d732cb8ab8d494eac8e894121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff02000000000000001976a91494677c56fa2968644c90a517214338b4139899ce88ac18aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7010000006a47304402207877451733ab726fc23288084dd99a6450490a70db06483c1709f7b482610cbc02207b7c84cca5e345a2a3a56fc59ba8ee12cadb856d3032a7aff7c3719c84048e144121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff03000000000000001976a91494677c56fa2968644c90a517214338b4139899ce88ac18aec743415be3827937f37e1bd6b1930e8d7cfbb22bcd72a373438399f1dcb7020000006b483045022100e500eba499e76490c6936c3ac13e1ed808edcd671d113f5fa76f23951116cbd40220576ff4993ce3ec757949aa0fc2bfe1b541660b520f0d1ed745cf79a210d9e3ff4121034d2d6d23fbcb6eefe3e80c47044e36797dcb80d0ac5e96e732ef03c3c550a116ffffffff04000000000000001976a91494677c56fa2968644c90a517214338b4139899ce88ac0301000000000000001976a9143cf53c49c322d9d811728182939aee2dca087f9888ac00000000000000000e006a0b68656c6c6f20776f726c6400000000000000000d6a0b68656c6c6f20776f726c6400000000", }, diff --git a/engine/tester/jsonrequire/funcs.go b/engine/tester/jsonrequire/funcs.go index 5aed60b91..b6e615ea7 100644 --- a/engine/tester/jsonrequire/funcs.go +++ b/engine/tester/jsonrequire/funcs.go @@ -6,10 +6,13 @@ import ( ) var funcsMap = template.FuncMap{ - "matchTimestamp": matchTimestamp, - "matchURL": matchURL, - "orEmpty": orEmpty, - "matchID64": matchID64, + "matchTimestamp": matchTimestamp, + "matchURL": matchURL, + "orEmpty": orEmpty, + "matchID64": matchID64, + "matchHexWithLength": matchHexWithLength, + "matchHex": matchHex, + "matchAddress": matchAddress, } func matchTimestamp() string { @@ -24,6 +27,18 @@ func matchID64() string { return `/^[a-zA-Z0-9]{64}$/` } +func matchHexWithLength(length int) string { + return fmt.Sprintf(`/^[a-f0-9]{%d}$/`, length) +} + +func matchHex() string { + return `/^[a-f0-9]+$/` +} + +func matchAddress() string { + return `/^(1|m)[a-km-zA-HJ-NP-Z1-9]{33}$/` +} + func orEmpty(statement string) string { if !containsRegex(statement) { return statement diff --git a/engine/tester/jsonrequire/getter.go b/engine/tester/jsonrequire/getter.go new file mode 100644 index 000000000..6ad7c5bd5 --- /dev/null +++ b/engine/tester/jsonrequire/getter.go @@ -0,0 +1,36 @@ +package jsonrequire + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +// Getter allows to get values from the JSON data. +type Getter struct { + t testing.TB + data map[string]any +} + +// NewGetterWithJSON creates a new Getter based on JSON string +func NewGetterWithJSON(t testing.TB, jsonString string) *Getter { + var data map[string]any + if err := json.Unmarshal([]byte(jsonString), &data); err != nil { + require.Fail(t, fmt.Sprintf("Provided value ('%s') is not valid json.\nJSON parsing error: '%s'", jsonString, err.Error())) + } + return &Getter{t: t, data: data} +} + +// GetString returns a string value from the data. +func (g *Getter) GetString(xpath string) string { + value := getByXPath(g.t, g.data, xpath) + + strValue, ok := value.(string) + if !ok { + require.Fail(g.t, "Value is not a string") + } + + return strValue +} diff --git a/engine/transaction/record/record_outline_test.go b/engine/transaction/record/record_outline_test.go index 6866ce6af..5640f4132 100644 --- a/engine/transaction/record/record_outline_test.go +++ b/engine/transaction/record/record_outline_test.go @@ -30,7 +30,7 @@ func givenTXWithOpReturn(t *testing.T) fixtures.GivenTXSpec { func givenTxWithOpReturnWithoutOPFalse(t *testing.T) fixtures.GivenTXSpec { return fixtures.GivenTX(t). WithInput(1). - WithOutputScript( + WithOutputScriptParts( fixtures.OpCode(script.OpRETURN), fixtures.PushData(dataOfOpReturnTx), ) @@ -162,7 +162,7 @@ func TestRecordOutlineOpReturnErrorCases(t *testing.T) { givenTxWithOpZeroAfterOpReturn := fixtures.GivenTX(t). WithInput(1). - WithOutputScript( + WithOutputScriptParts( fixtures.OpCode(script.OpFALSE), fixtures.OpCode(script.OpRETURN), fixtures.PushData(dataOfOpReturnTx), diff --git a/server/server.go b/server/server.go index 6a7a2910f..8e9fc9d9c 100644 --- a/server/server.go +++ b/server/server.go @@ -4,6 +4,7 @@ package server import ( "context" "crypto/tls" + "github.com/bitcoin-sv/spv-wallet/actions/paymailserver" "net/http" "strconv" @@ -107,8 +108,7 @@ func (s *Server) Handlers() *gin.Engine { func setupServerRoutes(appConfig *config.AppConfig, spvWalletEngine engine.ClientInterface, ginEngine *gin.Engine) { handlersManager := handlers.NewManager(ginEngine, appConfig) actions.Register(handlersManager) - - spvWalletEngine.GetPaymailConfig().RegisterRoutes(ginEngine) + paymailserver.Register(spvWalletEngine.GetPaymailConfig().Configuration, ginEngine) if appConfig.DebugProfiling { pprof.Register(ginEngine, "debug/pprof")