Skip to content

Commit e2c1393

Browse files
committed
fix(open_crypto_pay): use quote payment id for commits
1 parent 7dbf859 commit e2c1393

4 files changed

Lines changed: 52 additions & 10 deletions

File tree

lib/pages/open_crypto_pay/open_crypto_pay_confirm_view.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ class _OpenCryptoPayConfirmViewState
290290
openCryptoPayCommit: OpenCryptoPayCommit(
291291
callbackUrl: widget.paymentDetails.callback,
292292
quoteId: widget.paymentDetails.quote!.id,
293+
paymentId: widget.paymentDetails.quote!.paymentId,
293294
method: widget.selectedMethod.method,
294295
asset: widget.selectedAsset.asset,
295296
expiresAt: expiresAt,
@@ -348,6 +349,7 @@ class _OpenCryptoPayConfirmViewState
348349
openCryptoPayCommit: OpenCryptoPayCommit(
349350
callbackUrl: widget.paymentDetails.callback,
350351
quoteId: widget.paymentDetails.quote!.id,
352+
paymentId: widget.paymentDetails.quote!.paymentId,
351353
method: widget.selectedMethod.method,
352354
asset: widget.selectedAsset.asset,
353355
expiresAt: expiresAt,

lib/services/open_crypto_pay/models.dart

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,24 @@ class OpenCryptoPayTransferMethod {
9999

100100
class OpenCryptoPayQuote {
101101
final String id;
102+
final String paymentId;
102103
final DateTime expiration;
103104

104-
OpenCryptoPayQuote({required this.id, required this.expiration});
105+
OpenCryptoPayQuote({
106+
required this.id,
107+
required this.paymentId,
108+
required this.expiration,
109+
});
105110

106111
factory OpenCryptoPayQuote.fromJson(Map<String, dynamic> json) {
112+
final paymentId = json['payment'] as String?;
113+
if (paymentId == null || paymentId.isEmpty) {
114+
throw Exception('OpenCryptoPay: quote payment id is missing');
115+
}
116+
107117
return OpenCryptoPayQuote(
108118
id: json['id'] as String,
119+
paymentId: paymentId,
109120
expiration: DateTime.parse(json['expiration'] as String),
110121
);
111122
}
@@ -220,11 +231,11 @@ class OpenCryptoPayTransactionDetails {
220231
}
221232
}
222233

223-
/// Context required to notify the provider of a broadcast transaction via
224-
/// the `/tx/` endpoint (derived from the payment details callback URL).
234+
/// Context required to notify the provider via the `/tx/{paymentId}` endpoint.
225235
class OpenCryptoPayCommit {
226236
final String callbackUrl;
227237
final String quoteId;
238+
final String paymentId;
228239
final String method;
229240
final String asset;
230241
final DateTime expiresAt;
@@ -237,6 +248,7 @@ class OpenCryptoPayCommit {
237248
const OpenCryptoPayCommit({
238249
required this.callbackUrl,
239250
required this.quoteId,
251+
required this.paymentId,
240252
required this.method,
241253
required this.asset,
242254
required this.expiresAt,

lib/services/open_crypto_pay/open_crypto_pay_api.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ class OpenCryptoPayApi {
126126
}
127127

128128
/// Notifies the provider of a locally broadcast transaction so the merchant
129-
/// side can settle the payment. The `/tx/` endpoint is derived from the
130-
/// payment details callback URL.
129+
/// side can settle the payment.
131130
Future<void> commitTxId({
132131
required OpenCryptoPayCommit commit,
133132
required String txId,
@@ -161,7 +160,7 @@ class OpenCryptoPayApi {
161160
required OpenCryptoPayCommit commit,
162161
required Map<String, String> queryParameters,
163162
}) async {
164-
final base = _commitEndpoint(commit.callbackUrl);
163+
final base = _commitEndpoint(commit.callbackUrl, commit.paymentId);
165164
_requireHttps(base, 'commit endpoint');
166165
final uri = base.replace(
167166
queryParameters: {
@@ -181,15 +180,19 @@ class OpenCryptoPayApi {
181180
}
182181
}
183182

184-
Uri _commitEndpoint(String callbackUrl) {
183+
Uri _commitEndpoint(String callbackUrl, String paymentId) {
185184
final callback = Uri.parse(callbackUrl);
185+
if (paymentId.isEmpty) {
186+
throw Exception('OpenCryptoPay: quote payment id is missing');
187+
}
186188
final segments = callback.pathSegments.toList();
187-
final cbIndex = segments.indexOf('cb');
189+
final cbIndex = segments.lastIndexOf('cb');
188190
if (cbIndex == -1) {
189191
throw Exception('OpenCryptoPay: callback URL does not contain /cb/');
190192
}
191-
segments[cbIndex] = 'tx';
192-
return callback.replace(pathSegments: segments);
193+
return callback.replace(
194+
pathSegments: [...segments.take(cbIndex), 'tx', paymentId],
195+
);
193196
}
194197

195198
Uri _redactedUri(Uri uri) {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:stackwallet/services/open_crypto_pay/models.dart';
3+
4+
void main() {
5+
test("parses quote payment id used for commit endpoint", () {
6+
final quote = OpenCryptoPayQuote.fromJson({
7+
"id": "quote-id",
8+
"payment": "payment-id",
9+
"expiration": "2026-04-28T12:00:00Z",
10+
});
11+
12+
expect(quote.id, "quote-id");
13+
expect(quote.paymentId, "payment-id");
14+
});
15+
16+
test("rejects quotes without a payment id", () {
17+
expect(
18+
() => OpenCryptoPayQuote.fromJson({
19+
"id": "quote-id",
20+
"expiration": "2026-04-28T12:00:00Z",
21+
}),
22+
throwsException,
23+
);
24+
});
25+
}

0 commit comments

Comments
 (0)