Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0471b6d
fix(paynym): handle empty/non-JSON response bodies in _post()
sneurlax Mar 23, 2026
92084c0
test(paynym): add PayNym.rs POST tests for endpoints
sneurlax Mar 23, 2026
8bcc165
fix(paynym): use Bitcoin message signature for PayNym claim
sneurlax Mar 24, 2026
ec0542f
fix(paynym): handle bool "claimed" field in PaynymClaim.fromMap
sneurlax Mar 24, 2026
2081773
fix(paynym): handle claim success check and add null-token guard
sneurlax Mar 24, 2026
290eae2
fix(linux): enable secp256k1 recovery module in build script
sneurlax Mar 24, 2026
0b9bbf1
fix(windows): enable secp256k1 recovery module in WSL build script
sneurlax Mar 24, 2026
ab43945
fix(windows): enable secp256k1 recovery module in batch build script
sneurlax Mar 24, 2026
e28f769
fix(paynym): handle 401 and empty body in claim() response
sneurlax Mar 26, 2026
89cc01f
fix(paynym): navigate to PaynymHomeView when nym already claimed
sneurlax Mar 26, 2026
e2754ad
fix(paynym): re-enable following and fix null-unsafe success checks
sneurlax Mar 26, 2026
7899229
fix(paynym): handle taproot inputs in notification tx parsing/building
sneurlax Mar 26, 2026
e5cefa9
feat(paynym): add P2TR (taproot) payment address support
sneurlax Mar 26, 2026
783dbde
TODO: merge https://github.com/cypherstack/bip47/pull/9 and update to…
sneurlax Apr 6, 2026
c648781
feat(paynym): add isTaproot param to getPaymentCode and enable on claim
sneurlax Mar 26, 2026
86432cd
feat(paynym): infer taproot capability from payment code feature byte
sneurlax Mar 26, 2026
783cb5e
test(paynym): add PaynymAccountLite taproot inference tests
sneurlax Mar 26, 2026
86e5c9c
Merge branch 'staging' into fix/paynym
sneurlax May 13, 2026
eebee9b
chore: update ref back to cypherstack#main
sneurlax May 13, 2026
5f9ca3a
chore: dart format
sneurlax May 13, 2026
cf7251b
Merge branch 'staging' into fix/paynym
julian-CStack May 13, 2026
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
45 changes: 32 additions & 13 deletions lib/models/paynym/paynym_account_lite.dart
Original file line number Diff line number Diff line change
@@ -1,38 +1,57 @@
/*
/*
* This file is part of Stack Wallet.
*
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/

import 'package:bip47/bip47.dart';
import 'package:bitcoindart/bitcoindart.dart' as bitcoindart;

class PaynymAccountLite {
final String nymId;
final String nymName;
final String code;
final bool segwit;
final bool taproot;

PaynymAccountLite(
this.nymId,
this.nymName,
this.code,
this.segwit,
);
this.segwit, {
this.taproot = false,
});

PaynymAccountLite.fromMap(Map<String, dynamic> map)
: nymId = map["nymId"] as String,
nymName = map["nymName"] as String,
code = map["code"] as String,
segwit = map["segwit"] as bool;
: nymId = map["nymId"] as String,
nymName = map["nymName"] as String,
code = map["code"] as String,
segwit = map["segwit"] as bool,
taproot = map["taproot"] as bool? ?? inferTaproot(map["code"] as String);

static bool inferTaproot(String paymentCodeString) {
try {
final pCode = PaymentCode.fromPaymentCode(
paymentCodeString,
networkType: bitcoindart.bitcoin,
);
return pCode.isTaprootEnabled();
} catch (_) {
return false;
}
}

Map<String, dynamic> toMap() => {
"nymId": nymId,
"nymName": nymName,
"code": code,
"segwit": segwit,
};
"nymId": nymId,
"nymName": nymName,
"code": code,
"segwit": segwit,
"taproot": taproot,
};

@override
String toString() {
Expand Down
9 changes: 3 additions & 6 deletions lib/models/paynym/paynym_claim.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@ class PaynymClaim {
PaynymClaim(this.claimed, this.token);

PaynymClaim.fromMap(Map<String, dynamic> map)
: claimed = map["claimed"] as String,
token = map["token"] as String;
: claimed = map["claimed"].toString(),
token = map["token"] as String;

Map<String, dynamic> toMap() => {
"claimed": claimed,
"token": token,
};
Map<String, dynamic> toMap() => {"claimed": claimed, "token": token};

@override
String toString() {
Expand Down
178 changes: 92 additions & 86 deletions lib/pages/paynym/paynym_claim_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ import 'dialogs/claiming_paynym_dialog.dart';
import 'paynym_home_view.dart';

class PaynymClaimView extends ConsumerStatefulWidget {
const PaynymClaimView({
super.key,
required this.walletId,
});
const PaynymClaimView({super.key, required this.walletId});

final String walletId;

Expand Down Expand Up @@ -80,23 +77,20 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
leading: Row(
children: [
Padding(
padding: const EdgeInsets.only(
left: 24,
right: 20,
),
padding: const EdgeInsets.only(left: 24, right: 20),
child: AppBarIconButton(
size: 32,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
color: Theme.of(
context,
).extension<StackColors>()!.textFieldDefaultBG,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.arrowLeft,
width: 18,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
color: Theme.of(
context,
).extension<StackColors>()!.topNavIconPrimary,
),
onPressed: Navigator.of(context).pop,
),
Expand All @@ -107,13 +101,8 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
height: 42,
color: Theme.of(context).extension<StackColors>()!.textDark,
),
const SizedBox(
width: 10,
),
Text(
"PayNym",
style: STextStyles.desktopH3(context),
),
const SizedBox(width: 10),
Text("PayNym", style: STextStyles.desktopH3(context)),
],
),
)
Expand All @@ -129,52 +118,36 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
body: ConditionalParent(
condition: !isDesktop,
builder: (child) => SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
child: Padding(padding: const EdgeInsets.all(16), child: child),
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => SizedBox(
width: 328,
child: child,
),
builder: (child) => SizedBox(width: 328, child: child),
child: Column(
children: [
const Spacer(
flex: 1,
),
const Spacer(flex: 1),
SvgPicture.asset(
Assets.svg.unclaimedPaynym,
width: MediaQuery.of(context).size.width / 2,
),
const SizedBox(
height: 20,
),
const SizedBox(height: 20),
Text(
"You do not have a PayNym yet.\nClaim yours now!",
style: isDesktop
? STextStyles.desktopSubtitleH2(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
color: Theme.of(
context,
).extension<StackColors>()!.textSubtitle1,
)
: STextStyles.baseXS(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
color: Theme.of(
context,
).extension<StackColors>()!.textSubtitle1,
),
textAlign: TextAlign.center,
),
if (isDesktop)
const SizedBox(
height: 30,
),
if (!isDesktop)
const Spacer(
flex: 2,
),
if (isDesktop) const SizedBox(height: 30),
if (!isDesktop) const Spacer(flex: 2),
PrimaryButton(
label: "Claim",
onPressed: () async {
Expand All @@ -187,13 +160,17 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
).then((value) => shouldCancel = value == true),
);

final wallet = ref.read(pWallets).getWallet(widget.walletId)
as PaynymInterface;
final wallet =
ref.read(pWallets).getWallet(widget.walletId)
as PaynymInterface;

if (shouldCancel) return;

// get payment code
final pCode = await wallet.getPaymentCode(isSegwit: false);
// get payment code with taproot + segwit feature bits
final pCode = await wallet.getPaymentCode(
isSegwit: true,
isTaproot: true,
);

if (shouldCancel) return;

Expand All @@ -206,45 +183,67 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
if (shouldCancel) return;

if (created.value!.claimed) {
// payment code already claimed
// payment code already claimed — load account and navigate
debugPrint("pcode already claimed!!");

// final account =
// await ref.read(paynymAPIProvider).nym(pCode.toString());
// if (!account.value!.segwit) {
// for (int i = 0; i < 100; i++) {
// final result = await _addSegwitCode(account.value!);
// if (result == true) {
// break;
// }
// }
// }
final account = await ref
.read(paynymAPIProvider)
.nym(pCode.toString());

if (mounted) {
if (shouldCancel) return;

if (account.value != null && mounted) {
ref.read(myPaynymAccountStateProvider.state).state =
account.value!;
if (isDesktop) {
Navigator.of(context, rootNavigator: true).pop();
Navigator.of(context).pop();
} else {
Navigator.of(context).popUntil(
ModalRoute.withName(
WalletView.routeName,
),
);
Navigator.of(
context,
).popUntil(ModalRoute.withName(WalletView.routeName));
}
await Navigator.of(context).pushNamed(
PaynymHomeView.routeName,
arguments: widget.walletId,
);
} else if (mounted) {
if (isDesktop) {
Navigator.of(context, rootNavigator: true).pop();
Navigator.of(context).pop();
} else {
Navigator.of(
context,
).popUntil(ModalRoute.withName(WalletView.routeName));
}
}
return;
}

if (shouldCancel) return;

final token =
await ref.read(paynymAPIProvider).token(pCode.toString());
final token = await ref
.read(paynymAPIProvider)
.token(pCode.toString());

debugPrint("token result: $token");

if (shouldCancel) return;

if (token.value == null) {
debugPrint("token fetch failed: ${token.message}");
if (mounted) {
Navigator.of(context, rootNavigator: isDesktop).pop();
}
return;
}

// sign token with notification private key
final signature =
await wallet.signStringWithNotificationKey(token.value!);
final signature = await wallet.signStringWithNotificationKey(
token.value!,
);

debugPrint("signature: $signature");

if (shouldCancel) return;

Expand All @@ -253,11 +252,16 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
.read(paynymAPIProvider)
.claim(token.value!, signature);

debugPrint("claim result: $claim");

if (shouldCancel) return;

if (claim.value?.claimed == pCode.toString()) {
final account =
await ref.read(paynymAPIProvider).nym(pCode.toString());
if (claim.statusCode == 200 ||
claim.value?.claimed == pCode.toString() ||
claim.value?.claimed == "true") {
final account = await ref
.read(paynymAPIProvider)
.nym(pCode.toString());
// if (!account.value!.segwit) {
// for (int i = 0; i < 100; i++) {
// final result = await _addSegwitCode(account.value!);
Expand All @@ -274,26 +278,28 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
Navigator.of(context, rootNavigator: true).pop();
Navigator.of(context).pop();
} else {
Navigator.of(context).popUntil(
ModalRoute.withName(
WalletView.routeName,
),
);
Navigator.of(
context,
).popUntil(ModalRoute.withName(WalletView.routeName));
}
await Navigator.of(context).pushNamed(
PaynymHomeView.routeName,
arguments: widget.walletId,
);
}
} else if (mounted && !shouldCancel) {
debugPrint(
"claim failed or mismatch: "
"claimed=${claim.value?.claimed}, "
"expected=${pCode.toString()}, "
"statusCode=${claim.statusCode}, "
"message=${claim.message}",
);
Navigator.of(context, rootNavigator: isDesktop).pop();
}
},
),
if (isDesktop)
const Spacer(
flex: 2,
),
if (isDesktop) const Spacer(flex: 2),
],
),
),
Expand Down
Loading
Loading