Skip to content

Commit

Permalink
backend: add support for xpub_required in AOPP.
Browse files Browse the repository at this point in the history
  • Loading branch information
bznein committed Feb 10, 2025
1 parent eb7ec05 commit 9fce63f
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 51 deletions.
12 changes: 12 additions & 0 deletions backend/aopp.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ type AOPP struct {
coinCode coinpkg.Code
// format is the requested format. Available for all states except aoppStateInactive.
format string
// XpubRequired is true if we need to include the extended public key in the callback.
XpubRequired bool `json:"xpubRequired"`
}

// AOPP returns the current AOPP state.
Expand Down Expand Up @@ -279,6 +281,10 @@ func (backend *Backend) handleAOPP(uri url.URL) {

backend.aopp.format = q.Get("format")

if q.Has("xpub_required") {
backend.aopp.XpubRequired = true
}

backend.aopp.State = aoppStateUserApproval
backend.notifyAOPP()
}
Expand Down Expand Up @@ -358,6 +364,10 @@ func (backend *Backend) aoppChooseAccount(code accountsTypes.Code) {
backend.notifyAOPP()

var signature []byte
var xpub string
if backend.aopp.XpubRequired {
xpub = account.Config().Config.SigningConfigurations[signingConfigIdx].ExtendedPublicKey().String()
}
switch account.Coin().Code() {
case coinpkg.CodeBTC:
sig, err := backend.keystore.SignBTCMessage(
Expand Down Expand Up @@ -402,10 +412,12 @@ func (backend *Backend) aoppChooseAccount(code accountsTypes.Code) {
Version int `json:"version"`
Address string `json:"address"`
Signature []byte `json:"signature"` // is base64 encoded
Xpub string `json:"xpub,omitempty"`
}{
Version: 0,
Address: addr.EncodeForHumans(),
Signature: signature,
Xpub: xpub,
})
if err != nil {
log.WithError(err).Error("JSON error")
Expand Down
111 changes: 65 additions & 46 deletions backend/aopp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,24 +102,28 @@ func TestAOPPSuccess(t *testing.T) {
keystoreHelper := software.NewKeystore(rootKey)

tests := []struct {
asset string
coinCode coinpkg.Code
format string
scriptType *signing.ScriptType
address string
addressID string
accountCode accountsTypes.Code
accountName string
asset string
coinCode coinpkg.Code
format string
scriptType *signing.ScriptType
address string
addressID string
accountCode accountsTypes.Code
accountName string
xpubRequired bool
expectedXpub string
}{
{
asset: "btc",
coinCode: coinpkg.CodeBTC,
format: "any", // defaults to p2wpkh
scriptType: scriptTypeRef(signing.ScriptTypeP2WPKH),
address: "bc1qxp6xr63t098rl9udlynrktq00un6vqduzjgua3",
addressID: "9959e354fad09a47b0a5b0ac8af1b5f95924526241689b3ed7c472e79d95bde6",
accountCode: "v0-55555555-btc-0",
accountName: "Bitcoin",
asset: "btc",
coinCode: coinpkg.CodeBTC,
format: "any", // defaults to p2wpkh
scriptType: scriptTypeRef(signing.ScriptTypeP2WPKH),
address: "bc1qxp6xr63t098rl9udlynrktq00un6vqduzjgua3",
addressID: "9959e354fad09a47b0a5b0ac8af1b5f95924526241689b3ed7c472e79d95bde6",
accountCode: "v0-55555555-btc-0",
accountName: "Bitcoin",
xpubRequired: true,
expectedXpub: "xpub6Cxa67Bfe1Aw5VvLM1Ppua9x28CXH1zUYoAuBzFRjR6hWnA6aUcny84KYkeVcZWnWXxKSkxCEyMA8xic54ydBPWm5oziXpsXq6nX8FELMQn",
},
{
asset: "btc",
Expand All @@ -142,17 +146,21 @@ func TestAOPPSuccess(t *testing.T) {
accountName: "Bitcoin",
},
{
asset: "eth",
coinCode: coinpkg.CodeETH,
format: "any",
address: "0xB7C853464BE7Ae39c366C9C2A9D4b95340a708c7",
addressID: "0xB7C853464BE7Ae39c366C9C2A9D4b95340a708c7",
accountCode: "v0-55555555-eth-0",
accountName: "Ethereum",
asset: "eth",
coinCode: coinpkg.CodeETH,
format: "any",
address: "0xB7C853464BE7Ae39c366C9C2A9D4b95340a708c7",
addressID: "0xB7C853464BE7Ae39c366C9C2A9D4b95340a708c7",
accountCode: "v0-55555555-eth-0",
accountName: "Ethereum",
xpubRequired: true,
expectedXpub: "xpub6GP83vJASH1kS7dQPWXFjVHDfYajopbG8U3j8peBH67CRCnb8QmDxZJfWpbgCQNHAzCDJ4MyVYjoh7Yv9yo7PQuZ9YyktgrtD9vmeo67Y4E",
},
}

for _, test := range tests {
b := newBackend(t, testnetDisabled, regtestDisabled)

t.Run("", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method)
Expand All @@ -163,16 +171,19 @@ func TestAOPPSuccess(t *testing.T) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)

jsonBody := fmt.Sprintf(`{"version": 0, "address": "%s", "signature": "c2lnbmF0dXJl"}`, test.address)
if test.xpubRequired {
jsonBody = fmt.Sprintf(`{"version": 0, "address": "%s", "signature": "c2lnbmF0dXJl", "xpub": "%s"}`, test.address, test.expectedXpub)
}
require.JSONEq(t,
fmt.Sprintf(`{"version": 0, "address": "%s", "signature": "c2lnbmF0dXJl"}`, test.address),
jsonBody,
string(body),
)
w.WriteHeader(http.StatusNoContent)
})
server := httptest.NewServer(handler)
defer server.Close()

b := newBackend(t, testnetDisabled, regtestDisabled)
defer b.Close()

// Add a second account so we can test the choosing-account step. If there is only one
Expand All @@ -194,27 +205,33 @@ func TestAOPPSuccess(t *testing.T) {
params.Set("format", test.format)
params.Set("callback", callback)

if test.xpubRequired {
params.Set("xpub_required", "1")
}

require.Equal(t, AOPP{State: aoppStateInactive}, b.AOPP())
b.HandleURI(uriPrefix + params.Encode())
require.Equal(t,
AOPP{
State: aoppStateUserApproval,
Callback: callback,
Message: dummyMsg,
coinCode: test.coinCode,
format: test.format,
State: aoppStateUserApproval,
Callback: callback,
Message: dummyMsg,
coinCode: test.coinCode,
format: test.format,
XpubRequired: test.xpubRequired,
},
b.AOPP(),
)

b.AOPPApprove()
require.Equal(t,
AOPP{
State: aoppStateAwaitingKeystore,
Callback: callback,
Message: dummyMsg,
coinCode: test.coinCode,
format: test.format,
State: aoppStateAwaitingKeystore,
Callback: callback,
Message: dummyMsg,
coinCode: test.coinCode,
format: test.format,
XpubRequired: test.xpubRequired,
},
b.AOPP(),
)
Expand All @@ -228,10 +245,11 @@ func TestAOPPSuccess(t *testing.T) {
{Name: test.accountName, Code: test.accountCode},
{Name: "Second account", Code: regularAccountCode(rootFingerprint1, test.coinCode, 1)},
},
Callback: callback,
Message: dummyMsg,
coinCode: test.coinCode,
format: test.format,
Callback: callback,
Message: dummyMsg,
coinCode: test.coinCode,
format: test.format,
XpubRequired: test.xpubRequired,
},
b.AOPP(),
)
Expand All @@ -244,13 +262,14 @@ func TestAOPPSuccess(t *testing.T) {
{Name: test.accountName, Code: test.accountCode},
{Name: "Second account", Code: regularAccountCode(rootFingerprint1, test.coinCode, 1)},
},
AccountCode: test.accountCode,
Address: test.address,
AddressID: test.addressID,
Callback: callback,
Message: dummyMsg,
coinCode: test.coinCode,
format: test.format,
AccountCode: test.accountCode,
Address: test.address,
AddressID: test.addressID,
Callback: callback,
Message: dummyMsg,
coinCode: test.coinCode,
format: test.format,
XpubRequired: test.xpubRequired,
},
b.AOPP(),
)
Expand Down
1 change: 1 addition & 0 deletions frontends/web/src/api/aopp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type Aopp = {
state: 'user-approval' | 'awaiting-keystore' | 'syncing';
message: string;
callback: string;
xpubRequired: boolean;
} | {
state: 'choosing-account';
accounts: Accounts;
Expand Down
17 changes: 14 additions & 3 deletions frontends/web/src/components/aopp/aopp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ export const Aopp = () => {
// Inactive, waiting for action.
return null;
case 'user-approval':
const host = domain(aopp.callback);
const addressRequestMsg = aopp.xpubRequired ? 'aopp.addressRequestWithXPub' : 'aopp.addressRequest';
const addressRequestWithLogoMsg = aopp.xpubRequired ? 'aopp.addressRequestWithLogoAndXPub' : 'aopp.addressRequestWithLogo';
return (
<View
fullscreen
Expand All @@ -109,11 +112,19 @@ export const Aopp = () => {
<Vasp prominent
hostname={domain(aopp.callback)}
fallback={(
<SimpleMarkup tagName="p" markup={t('aopp.addressRequest', {
host: `<strong>${domain(aopp.callback)}</strong>`
<SimpleMarkup tagName="p" markup={t(addressRequestMsg, {
host: `<strong>${host}</strong>`
})} />
)}
withLogoText={t('aopp.addressRequestWithLogo')} />
withLogoText={t(addressRequestWithLogoMsg)} />
{
aopp.xpubRequired ?
(
<div>
<Message type="info"> {t('aopp.xpubRequested', { host: `${host}` })} </Message>
</div>
) : ''
}
</ViewContent>
<ViewButtons>
<Button primary onClick={aoppAPI.approve}>{t('button.continue')}</Button>
Expand Down
7 changes: 5 additions & 2 deletions frontends/web/src/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@
},
"aopp": {
"addressRequest": "{{host}} is requesting a receiving address.",
"addressRequestWithLogo": "is requesting a receiving address",
"addressRequestWithLogo": "is requesting a receiving address.",
"addressRequestWithLogoAndXPub": "is requesting a receiving address and xPub.",
"addressRequestWithXPub": "{{host}} is requesting a receiving address and xPub.",
"banner": "Address request in progress. Please connect your device to continue.",
"errorTitle": "Error during address request ",
"labelAddress": "Address",
Expand All @@ -84,7 +86,8 @@
"title": "Address successfully sent"
},
"syncing": "Syncing the account, please wait.",
"title": "Address request"
"title": "Address request",
"xpubRequested": "Sharing your xPub lets external services see your account addresses. Your coins are still safe and remain in your control."
},
"app": {
"upgrade": "A new version of this app is available! Please upgrade from {{current}} to {{version}}."
Expand Down

0 comments on commit 9fce63f

Please sign in to comment.