Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SPV-1337): new transaction flow - type 42; paymail; operations #837

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1062814
feat(SPV-1337): new paymail service provider
chris-4chain Dec 30, 2024
562dca3
feat(SPV-1337): paymail endpoint tests
chris-4chain Dec 30, 2024
ac322ba
feat(SPV-1337): paymailserver package
chris-4chain Dec 30, 2024
8887fbf
feat(SPV-1337): paymail verify capability
chris-4chain Dec 30, 2024
c119bd2
feat(SPV-1337): incoming paymail tx test
chris-4chain Dec 30, 2024
56aa0d9
feat(SPV-1337): JSON value getter
chris-4chain Dec 30, 2024
135b63d
feat(SPV-1337): TestIncomingPaymailRawTX for old flow
chris-4chain Dec 30, 2024
2b4e269
feat(SPV-1337): lint
chris-4chain Dec 30, 2024
ee7880a
feat(SPV-1337): create destination
chris-4chain Dec 31, 2024
f2c50e3
feat(SPV-1337): record raw tx for incoming paymail
chris-4chain Dec 31, 2024
960c108
feat(SPV-1337): tests for incoming beef-paymail tx
chris-4chain Dec 31, 2024
c2b7acf
feat(SPV-1337): handle beef tx without storing missing transactions
chris-4chain Dec 31, 2024
50bdd44
feat(SPV-1337): tx tracker for missing transactions in beef
chris-4chain Dec 31, 2024
40b3a00
feat(SPV-1337): new operations table
chris-4chain Jan 2, 2025
665a6a1
feat(SPV-1337): adjust utxo selector and fix test suites
chris-4chain Jan 2, 2025
5eeb342
feat(SPV-1337): rename output to tracked output
chris-4chain Jan 2, 2025
8fcecf4
feat(SPV-1337): virtual output concept
chris-4chain Jan 3, 2025
5756ac5
feat(SPV-1337): timestamps for models
chris-4chain Jan 3, 2025
4fc8ac6
feat(SPV-1337): store UserUTXOs
chris-4chain Jan 3, 2025
e4605e1
feat(SPV-1345): operation oriented record transaction
chris-4chain Jan 3, 2025
dad2043
feat(SPV-1337): balance and operation api endpoints
chris-4chain Jan 3, 2025
7ac376b
feat(SPV-1337): linter
chris-4chain Jan 3, 2025
b007593
Merge branch 'main' into feat/SPV-1337
chris-4chain Jan 3, 2025
b1cdfa5
feat(SPV-1337): self review
chris-4chain Jan 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
320 changes: 320 additions & 0 deletions actions/paymailserver/incoming_paymail_tx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
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(),
testengine.WithNewTransactionFlowEnabled(),
)
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 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,
})

// update:
testState.txID = txSpec.ID()
})

t.Run("step 3 - check balance", func(t *testing.T) {
// given:
senderClient := given.HttpClient().ForGivenUser(fixtures.Sender)

// when:
res, _ := senderClient.R().Get("/api/v2/users/balance")

// then:
then.Response(res).IsOK().WithJSONf(`{
"currentBalance": %d
}`, satoshis)
})

t.Run("step 4 - get operations", func(t *testing.T) {
// given:
senderClient := given.HttpClient().ForGivenUser(fixtures.Sender)

// when:
res, _ := senderClient.R().Get("/api/v2/users/operations")

// then:
then.Response(res).IsOK().WithJSONMatching(`{
"content": [
{
"txID": "{{ .txID }}",
"time": "{{ matchTimestamp }}",
"value": {{ .value }}
}
],
"page": {
"number": 1,
"size": 1,
"totalElements": 1,
"totalPages": 1
}
}`, map[string]any{
"value": satoshis,
"txID": testState.txID,
})
})
}

func TestIncomingPaymailBeef(t *testing.T) {
givenForAllTests := testabilities.Given(t)
cleanup := givenForAllTests.StartedSPVWalletWithConfiguration(
testengine.WithDomainValidationDisabled(),
testengine.WithNewTransactionFlowEnabled(),
)
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,
})

// update:
testState.txID = txSpec.ID()
})

t.Run("step 3 - check balance", func(t *testing.T) {
// given:
senderClient := given.HttpClient().ForGivenUser(fixtures.Sender)

// when:
res, _ := senderClient.R().Get("/api/v2/users/balance")

// then:
then.Response(res).IsOK().WithJSONf(`{
"currentBalance": %d
}`, satoshis)
})

t.Run("step 4 - get operations", func(t *testing.T) {
// given:
senderClient := given.HttpClient().ForGivenUser(fixtures.Sender)

// when:
res, _ := senderClient.R().Get("/api/v2/users/operations")

// then:
then.Response(res).IsOK().WithJSONMatching(`{
"content": [
{
"txID": "{{ .txID }}",
"time": "{{ matchTimestamp }}",
"value": {{ .value }}
}
],
"page": {
"number": 1,
"size": 1,
"totalElements": 1,
"totalPages": 1
}
}`, map[string]any{
"value": satoshis,
"txID": testState.txID,
})
})
}
11 changes: 11 additions & 0 deletions actions/paymailserver/register.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading