Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 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
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
29 changes: 24 additions & 5 deletions lib/models/paynym/paynym_account_lite.dart
Original file line number Diff line number Diff line change
@@ -1,37 +1,56 @@
/*
/*
* 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;
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,
"taproot": taproot,
};

@override
Expand Down
2 changes: 1 addition & 1 deletion lib/models/paynym/paynym_claim.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class PaynymClaim {
PaynymClaim(this.claimed, this.token);

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

Map<String, dynamic> toMap() => {
Expand Down
68 changes: 53 additions & 15 deletions lib/pages/paynym/paynym_claim_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,11 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {

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,21 +209,33 @@ 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,
),
);
}
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();
Expand All @@ -240,22 +255,38 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
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!);

debugPrint("signature: $signature");

if (shouldCancel) return;

// claim paynym account
final claim = await ref
.read(paynymAPIProvider)
.claim(token.value!, signature);

debugPrint("claim result: $claim");

if (shouldCancel) return;

if (claim.value?.claimed == 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) {
Expand Down Expand Up @@ -286,6 +317,13 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
);
}
} 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();
}
},
Expand Down
21 changes: 16 additions & 5 deletions lib/utilities/paynym_is_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,16 @@ class PaynymIsApi {
// debugPrint("Paynym response code: ${response.code}");
// debugPrint("Paynym response body: ${response.body}");

return Tuple2(
jsonDecode(response.body) as Map<String, dynamic>,
response.code,
);
Map<String, dynamic> parsedBody;
try {
final bodyStr = response.body.trim();
parsedBody = bodyStr.isEmpty
? {}
: jsonDecode(bodyStr) as Map<String, dynamic>;
} catch (_) {
parsedBody = {};
}
return Tuple2(parsedBody, response.code);
}

// ### `/api/v1/create`
Expand Down Expand Up @@ -357,11 +363,16 @@ class PaynymIsApi {
switch (result.item2) {
case 200:
message = "Payment code successfully claimed";
value = PaynymClaim.fromMap(result.item1);
if (result.item1.isNotEmpty) {
value = PaynymClaim.fromMap(result.item1);
}
break;
case 400:
message = "Bad request";
break;
case 401:
message = "Unauthorized token or signature";
break;
default:
message = result.item1["message"] as String? ?? "Unknown error";
}
Expand Down
Loading
Loading