@perkos/agent-sdk 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +54 -2
- package/SECURITY.md +10 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mpp.d.ts +135 -0
- package/dist/mpp.d.ts.map +1 -0
- package/dist/mpp.js +784 -0
- package/dist/mpp.js.map +1 -0
- package/docs/ARCHITECTURE.md +20 -1
- package/docs/MPP_PAYMENTS.md +155 -0
- package/examples/mpp-usdcx.ts +91 -0
- package/package.json +7 -1
package/dist/mpp.js
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
import { AnchorMode, AuthType, deserializeTransaction, isSingleSig, transactionToHex, validateStacksAddress, } from "@stacks/transactions";
|
|
2
|
+
import { createNayoriX402DirectPaymentPayload, createNayoriX402PaymentRequirements, getNayoriX402Asset, verifyNayoriX402DirectPayment, } from "./x402-direct.js";
|
|
3
|
+
import { buildNayoriX402UnsignedPaymentTransaction } from "./x402-paying.js";
|
|
4
|
+
import { fromStacksX402Network } from "./x402.js";
|
|
5
|
+
import { normalizeTxid } from "./txid.js";
|
|
6
|
+
export const MPP_PAYMENT_SCHEME = "Payment";
|
|
7
|
+
export const NAYORI_MPP_METHOD = "usdc";
|
|
8
|
+
export const NAYORI_MPP_INTENT = "charge";
|
|
9
|
+
export const NAYORI_MPP_PROFILE = "stacks";
|
|
10
|
+
export const NAYORI_MPP_CREDENTIAL_HEADER = "Payment-Authorization";
|
|
11
|
+
export const NAYORI_MPP_TRANSACTION_FORMAT = "stacks_transaction_v1";
|
|
12
|
+
const MAX_ENCODED_ENVELOPE_CHARACTERS = 131_072;
|
|
13
|
+
const MAX_TRANSACTION_BYTES = 16_384;
|
|
14
|
+
const SECP256K1_HALF_ORDER = 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0n;
|
|
15
|
+
const TOKEN_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
16
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
17
|
+
const BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
18
|
+
const DECIMAL_PATTERN = /^(?:0|[1-9][0-9]*)$/;
|
|
19
|
+
const CHALLENGE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
20
|
+
export class NayoriMppVerificationError extends Error {
|
|
21
|
+
reason;
|
|
22
|
+
details;
|
|
23
|
+
constructor(reason, message, details) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "NayoriMppVerificationError";
|
|
26
|
+
this.reason = reason;
|
|
27
|
+
this.details = details;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function mppError(reason, message, details) {
|
|
31
|
+
return new NayoriMppVerificationError(reason, message, details);
|
|
32
|
+
}
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function exactKeys(value, allowed, field) {
|
|
37
|
+
const allowedKeys = new Set(allowed);
|
|
38
|
+
for (const key of Object.keys(value)) {
|
|
39
|
+
if (!allowedKeys.has(key)) {
|
|
40
|
+
throw mppError("invalid_envelope", `${field} contains unsupported field ${key}.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function requireString(value, field, options = {}) {
|
|
45
|
+
if (typeof value !== "string" ||
|
|
46
|
+
value.length === 0 ||
|
|
47
|
+
value.length > (options.max ?? 16_384) ||
|
|
48
|
+
(options.pattern && !options.pattern.test(value))) {
|
|
49
|
+
throw mppError("invalid_envelope", `${field} is invalid.`);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function requireHttpText(value, field, max) {
|
|
54
|
+
const text = requireString(value, field, { max });
|
|
55
|
+
for (const character of text) {
|
|
56
|
+
const point = character.codePointAt(0);
|
|
57
|
+
if (point < 0x20 || point === 0x7f) {
|
|
58
|
+
throw mppError("invalid_header", `${field} must not contain HTTP control characters.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return text;
|
|
62
|
+
}
|
|
63
|
+
function assertWellFormedUnicode(value) {
|
|
64
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
65
|
+
const unit = value.charCodeAt(index);
|
|
66
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
67
|
+
const next = value.charCodeAt(index + 1);
|
|
68
|
+
if (!Number.isInteger(next) || next < 0xdc00 || next > 0xdfff) {
|
|
69
|
+
throw mppError("invalid_jcs", "JCS strings must not contain lone Unicode surrogates.");
|
|
70
|
+
}
|
|
71
|
+
index += 1;
|
|
72
|
+
}
|
|
73
|
+
else if (unit >= 0xdc00 && unit <= 0xdfff) {
|
|
74
|
+
throw mppError("invalid_jcs", "JCS strings must not contain lone Unicode surrogates.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function canonicalizeValue(value, stack) {
|
|
79
|
+
if (value === null || typeof value === "boolean") {
|
|
80
|
+
return JSON.stringify(value);
|
|
81
|
+
}
|
|
82
|
+
if (typeof value === "string") {
|
|
83
|
+
assertWellFormedUnicode(value);
|
|
84
|
+
return JSON.stringify(value);
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === "number") {
|
|
87
|
+
if (!Number.isFinite(value)) {
|
|
88
|
+
throw mppError("invalid_jcs", "JCS numbers must be finite.");
|
|
89
|
+
}
|
|
90
|
+
return JSON.stringify(value);
|
|
91
|
+
}
|
|
92
|
+
if (typeof value !== "object") {
|
|
93
|
+
throw mppError("invalid_jcs", "JCS values must be valid JSON values.");
|
|
94
|
+
}
|
|
95
|
+
if (stack.has(value)) {
|
|
96
|
+
throw mppError("invalid_jcs", "JCS values must not contain cycles.");
|
|
97
|
+
}
|
|
98
|
+
stack.add(value);
|
|
99
|
+
try {
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return `[${value.map(item => canonicalizeValue(item, stack)).join(",")}]`;
|
|
102
|
+
}
|
|
103
|
+
const object = value;
|
|
104
|
+
const prototype = Object.getPrototypeOf(object);
|
|
105
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
106
|
+
throw mppError("invalid_jcs", "JCS objects must be plain JSON objects.");
|
|
107
|
+
}
|
|
108
|
+
const members = Object.keys(object)
|
|
109
|
+
.sort()
|
|
110
|
+
.map(key => {
|
|
111
|
+
assertWellFormedUnicode(key);
|
|
112
|
+
return `${JSON.stringify(key)}:${canonicalizeValue(object[key], stack)}`;
|
|
113
|
+
});
|
|
114
|
+
return `{${members.join(",")}}`;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
stack.delete(value);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** RFC 8785 JSON Canonicalization Scheme for JSON-compatible values. */
|
|
121
|
+
export function canonicalizeNayoriMppJson(value) {
|
|
122
|
+
return canonicalizeValue(value, new Set());
|
|
123
|
+
}
|
|
124
|
+
function bytesToBinary(bytes) {
|
|
125
|
+
let result = "";
|
|
126
|
+
const chunkSize = 0x8000;
|
|
127
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
128
|
+
const chunk = bytes.subarray(offset, offset + chunkSize);
|
|
129
|
+
for (const byte of chunk)
|
|
130
|
+
result += String.fromCharCode(byte);
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
function binaryToBytes(binary) {
|
|
135
|
+
const bytes = new Uint8Array(binary.length);
|
|
136
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
137
|
+
bytes[index] = binary.charCodeAt(index);
|
|
138
|
+
}
|
|
139
|
+
return bytes;
|
|
140
|
+
}
|
|
141
|
+
function base64UrlFromBytes(bytes) {
|
|
142
|
+
return btoa(bytesToBinary(bytes)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
143
|
+
}
|
|
144
|
+
function bytesFromBase64Url(value, field) {
|
|
145
|
+
if (value.length === 0 ||
|
|
146
|
+
value.length > MAX_ENCODED_ENVELOPE_CHARACTERS ||
|
|
147
|
+
!BASE64URL_PATTERN.test(value) ||
|
|
148
|
+
value.length % 4 === 1) {
|
|
149
|
+
throw mppError("invalid_base64url", `${field} is not canonical base64url without padding.`);
|
|
150
|
+
}
|
|
151
|
+
const standard = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
152
|
+
const padded = standard + "=".repeat((4 - (standard.length % 4)) % 4);
|
|
153
|
+
try {
|
|
154
|
+
const bytes = binaryToBytes(atob(padded));
|
|
155
|
+
if (base64UrlFromBytes(bytes) !== value) {
|
|
156
|
+
throw mppError("invalid_base64url", `${field} is not canonical base64url without padding.`);
|
|
157
|
+
}
|
|
158
|
+
return bytes;
|
|
159
|
+
}
|
|
160
|
+
catch (cause) {
|
|
161
|
+
if (cause instanceof NayoriMppVerificationError)
|
|
162
|
+
throw cause;
|
|
163
|
+
throw mppError("invalid_base64url", `${field} cannot be decoded.`, { cause });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export function encodeNayoriMppJson(value) {
|
|
167
|
+
return base64UrlFromBytes(new TextEncoder().encode(canonicalizeNayoriMppJson(value)));
|
|
168
|
+
}
|
|
169
|
+
export function decodeNayoriMppJson(value, field = "MPP envelope") {
|
|
170
|
+
const bytes = bytesFromBase64Url(value, field);
|
|
171
|
+
let text;
|
|
172
|
+
try {
|
|
173
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
174
|
+
}
|
|
175
|
+
catch (cause) {
|
|
176
|
+
throw mppError("invalid_json", `${field} is not valid UTF-8.`, { cause });
|
|
177
|
+
}
|
|
178
|
+
let decoded;
|
|
179
|
+
try {
|
|
180
|
+
decoded = JSON.parse(text);
|
|
181
|
+
}
|
|
182
|
+
catch (cause) {
|
|
183
|
+
throw mppError("invalid_json", `${field} is not valid JSON.`, { cause });
|
|
184
|
+
}
|
|
185
|
+
if (canonicalizeNayoriMppJson(decoded) !== text) {
|
|
186
|
+
throw mppError("non_canonical_json", `${field} is not RFC 8785 canonical JSON.`);
|
|
187
|
+
}
|
|
188
|
+
return decoded;
|
|
189
|
+
}
|
|
190
|
+
function normalizeRfc3339(value, field) {
|
|
191
|
+
const text = requireString(value, field, { max: 64 });
|
|
192
|
+
const timestamp = Date.parse(text);
|
|
193
|
+
if (!Number.isFinite(timestamp) ||
|
|
194
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(text)) {
|
|
195
|
+
throw mppError("invalid_envelope", `${field} must be an RFC3339 timestamp.`);
|
|
196
|
+
}
|
|
197
|
+
return text;
|
|
198
|
+
}
|
|
199
|
+
function normalizeChallenge(value) {
|
|
200
|
+
if (!isRecord(value))
|
|
201
|
+
throw mppError("invalid_challenge", "MPP challenge must be an object.");
|
|
202
|
+
exactKeys(value, ["id", "realm", "method", "intent", "request", "expires", "digest", "header", "description"], "challenge");
|
|
203
|
+
const method = requireString(value.method, "challenge.method", { max: 32 });
|
|
204
|
+
const intent = requireString(value.intent, "challenge.intent", { max: 32 });
|
|
205
|
+
const header = requireString(value.header, "challenge.header", { max: 64 });
|
|
206
|
+
if (method !== NAYORI_MPP_METHOD || intent !== NAYORI_MPP_INTENT) {
|
|
207
|
+
throw mppError("unsupported_method", "Nayori supports MPP method=usdc and intent=charge.");
|
|
208
|
+
}
|
|
209
|
+
if (header !== NAYORI_MPP_CREDENTIAL_HEADER) {
|
|
210
|
+
throw mppError("invalid_credential_header", "Nayori MPP challenges require Payment-Authorization so Bearer authentication remains separate.");
|
|
211
|
+
}
|
|
212
|
+
const challenge = {
|
|
213
|
+
id: requireString(value.id, "challenge.id", { max: 128, pattern: CHALLENGE_ID_PATTERN }),
|
|
214
|
+
realm: requireHttpText(value.realm, "challenge.realm", 255),
|
|
215
|
+
method: NAYORI_MPP_METHOD,
|
|
216
|
+
intent: NAYORI_MPP_INTENT,
|
|
217
|
+
request: requireString(value.request, "challenge.request", {
|
|
218
|
+
max: MAX_ENCODED_ENVELOPE_CHARACTERS,
|
|
219
|
+
pattern: BASE64URL_PATTERN,
|
|
220
|
+
}),
|
|
221
|
+
expires: normalizeRfc3339(value.expires, "challenge.expires"),
|
|
222
|
+
digest: requireString(value.digest, "challenge.digest", {
|
|
223
|
+
max: 128,
|
|
224
|
+
pattern: /^sha-256=:[A-Za-z0-9+/]{43}=:$/,
|
|
225
|
+
}),
|
|
226
|
+
header: NAYORI_MPP_CREDENTIAL_HEADER,
|
|
227
|
+
...(value.description === undefined
|
|
228
|
+
? {}
|
|
229
|
+
: { description: requireHttpText(value.description, "challenge.description", 512) }),
|
|
230
|
+
};
|
|
231
|
+
decodeNayoriMppJson(challenge.request, "challenge.request");
|
|
232
|
+
return Object.freeze(challenge);
|
|
233
|
+
}
|
|
234
|
+
function parseCurrency(currency) {
|
|
235
|
+
const match = /^([^.]+)\.([a-z][a-z0-9-]{0,39})::([a-zA-Z][a-zA-Z0-9-]{0,127})$/.exec(currency);
|
|
236
|
+
if (!match?.[1] || !match[2] || !match[3]) {
|
|
237
|
+
throw mppError("invalid_payment_request", "MPP currency must be a full SIP-010 asset identifier.");
|
|
238
|
+
}
|
|
239
|
+
return { contractAddress: match[1], contractName: match[2], assetName: match[3] };
|
|
240
|
+
}
|
|
241
|
+
export function decodeNayoriMppUsdcStacksRequest(encoded) {
|
|
242
|
+
const value = decodeNayoriMppJson(encoded, "MPP payment request");
|
|
243
|
+
if (!isRecord(value)) {
|
|
244
|
+
throw mppError("invalid_payment_request", "MPP payment request must be an object.");
|
|
245
|
+
}
|
|
246
|
+
exactKeys(value, ["amount", "currency", "recipient", "description", "externalId", "methodDetails"], "payment request");
|
|
247
|
+
const amount = requireString(value.amount, "payment request.amount", {
|
|
248
|
+
max: 78,
|
|
249
|
+
pattern: DECIMAL_PATTERN,
|
|
250
|
+
});
|
|
251
|
+
if (amount === "0") {
|
|
252
|
+
throw mppError("invalid_payment_request", "payment request.amount must be greater than zero.");
|
|
253
|
+
}
|
|
254
|
+
const currency = requireString(value.currency, "payment request.currency", { max: 256 });
|
|
255
|
+
const currencyParts = parseCurrency(currency);
|
|
256
|
+
const recipient = requireString(value.recipient, "payment request.recipient", { max: 64 });
|
|
257
|
+
const externalId = requireString(value.externalId, "payment request.externalId", { max: 128 });
|
|
258
|
+
if (!isRecord(value.methodDetails)) {
|
|
259
|
+
throw mppError("invalid_payment_request", "payment request.methodDetails must be an object.");
|
|
260
|
+
}
|
|
261
|
+
exactKeys(value.methodDetails, ["type", "stacks"], "payment request.methodDetails");
|
|
262
|
+
if (value.methodDetails.type !== NAYORI_MPP_PROFILE || !isRecord(value.methodDetails.stacks)) {
|
|
263
|
+
throw mppError("unsupported_profile", "Nayori MPP requires methodDetails.type=stacks.");
|
|
264
|
+
}
|
|
265
|
+
const stacks = value.methodDetails.stacks;
|
|
266
|
+
exactKeys(stacks, [
|
|
267
|
+
"network",
|
|
268
|
+
"chainId",
|
|
269
|
+
"contractAddress",
|
|
270
|
+
"contractName",
|
|
271
|
+
"assetName",
|
|
272
|
+
"functionName",
|
|
273
|
+
"decimals",
|
|
274
|
+
"feePayer",
|
|
275
|
+
], "payment request.methodDetails.stacks");
|
|
276
|
+
const network = stacks.network;
|
|
277
|
+
const chainId = stacks.chainId;
|
|
278
|
+
if ((network !== "mainnet" && network !== "testnet") ||
|
|
279
|
+
(chainId !== "1" && chainId !== "2147483648") ||
|
|
280
|
+
(network === "mainnet" ? chainId !== "1" : chainId !== "2147483648")) {
|
|
281
|
+
throw mppError("network_mismatch", "MPP Stacks network and chainId do not match.");
|
|
282
|
+
}
|
|
283
|
+
const contractAddress = requireString(stacks.contractAddress, "stacks.contractAddress", {
|
|
284
|
+
max: 64,
|
|
285
|
+
});
|
|
286
|
+
const contractName = requireString(stacks.contractName, "stacks.contractName", { max: 40 });
|
|
287
|
+
const assetName = requireString(stacks.assetName, "stacks.assetName", { max: 128 });
|
|
288
|
+
if (!validateStacksAddress(contractAddress) ||
|
|
289
|
+
stacks.functionName !== "transfer" ||
|
|
290
|
+
stacks.decimals !== 6 ||
|
|
291
|
+
stacks.feePayer !== false ||
|
|
292
|
+
assetName !== "usdcx-token") {
|
|
293
|
+
throw mppError("invalid_payment_request", "MPP Stacks USDCx method details are invalid.");
|
|
294
|
+
}
|
|
295
|
+
if (currencyParts.contractAddress !== contractAddress ||
|
|
296
|
+
currencyParts.contractName !== contractName ||
|
|
297
|
+
currencyParts.assetName !== assetName) {
|
|
298
|
+
throw mppError("asset_mismatch", "MPP currency and Stacks token tuple do not match.");
|
|
299
|
+
}
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
amount,
|
|
302
|
+
currency,
|
|
303
|
+
recipient,
|
|
304
|
+
...(value.description === undefined
|
|
305
|
+
? {}
|
|
306
|
+
: { description: requireString(value.description, "payment request.description", { max: 512 }) }),
|
|
307
|
+
externalId,
|
|
308
|
+
methodDetails: Object.freeze({
|
|
309
|
+
type: NAYORI_MPP_PROFILE,
|
|
310
|
+
stacks: Object.freeze({
|
|
311
|
+
network,
|
|
312
|
+
chainId,
|
|
313
|
+
contractAddress,
|
|
314
|
+
contractName,
|
|
315
|
+
assetName: "usdcx-token",
|
|
316
|
+
functionName: "transfer",
|
|
317
|
+
decimals: 6,
|
|
318
|
+
feePayer: false,
|
|
319
|
+
}),
|
|
320
|
+
}),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function digestFromSha256Hex(value) {
|
|
324
|
+
if (!/^[0-9a-f]{64}$/.test(value)) {
|
|
325
|
+
throw mppError("invalid_quote", "The trusted quote body digest is invalid.");
|
|
326
|
+
}
|
|
327
|
+
const bytes = new Uint8Array(32);
|
|
328
|
+
for (let index = 0; index < 32; index += 1) {
|
|
329
|
+
bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
|
|
330
|
+
}
|
|
331
|
+
return `sha-256=:${btoa(bytesToBinary(bytes))}:`;
|
|
332
|
+
}
|
|
333
|
+
function quotedHeaderValue(value) {
|
|
334
|
+
for (const character of value) {
|
|
335
|
+
const point = character.codePointAt(0);
|
|
336
|
+
if (point < 0x20 || point === 0x7f) {
|
|
337
|
+
throw mppError("invalid_header", "MPP header values must not contain control characters.");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
341
|
+
}
|
|
342
|
+
export function encodeNayoriMppChallengeHeader(challengeInput) {
|
|
343
|
+
const challenge = normalizeChallenge(challengeInput);
|
|
344
|
+
const parameters = [
|
|
345
|
+
["id", challenge.id],
|
|
346
|
+
["realm", challenge.realm],
|
|
347
|
+
["method", challenge.method],
|
|
348
|
+
["intent", challenge.intent],
|
|
349
|
+
["request", challenge.request],
|
|
350
|
+
["expires", challenge.expires],
|
|
351
|
+
["digest", challenge.digest],
|
|
352
|
+
["header", challenge.header],
|
|
353
|
+
...(challenge.description ? [["description", challenge.description]] : []),
|
|
354
|
+
];
|
|
355
|
+
return `${MPP_PAYMENT_SCHEME} ${parameters
|
|
356
|
+
.map(([name, value]) => `${name}=${quotedHeaderValue(value)}`)
|
|
357
|
+
.join(", ")}`;
|
|
358
|
+
}
|
|
359
|
+
export function decodeNayoriMppChallengeHeader(value) {
|
|
360
|
+
const prefix = `${MPP_PAYMENT_SCHEME} `;
|
|
361
|
+
if (!value.startsWith(prefix)) {
|
|
362
|
+
throw mppError("invalid_header", "WWW-Authenticate must use the Payment scheme.");
|
|
363
|
+
}
|
|
364
|
+
const input = value.slice(prefix.length);
|
|
365
|
+
const parameters = {};
|
|
366
|
+
let index = 0;
|
|
367
|
+
while (index < input.length) {
|
|
368
|
+
while (input[index] === " " || input[index] === "\t")
|
|
369
|
+
index += 1;
|
|
370
|
+
if (index >= input.length || input[index] === ",") {
|
|
371
|
+
throw mppError("invalid_header", "MPP challenge contains an empty auth-param.");
|
|
372
|
+
}
|
|
373
|
+
const start = index;
|
|
374
|
+
while (index < input.length && TOKEN_PATTERN.test(input[index]))
|
|
375
|
+
index += 1;
|
|
376
|
+
const name = input.slice(start, index);
|
|
377
|
+
if (!name || input[index] !== "=") {
|
|
378
|
+
throw mppError("invalid_header", "MPP challenge auth-param is malformed.");
|
|
379
|
+
}
|
|
380
|
+
index += 1;
|
|
381
|
+
if (input[index] !== '"') {
|
|
382
|
+
throw mppError("invalid_header", "MPP challenge auth-param values must be quoted.");
|
|
383
|
+
}
|
|
384
|
+
index += 1;
|
|
385
|
+
let parsed = "";
|
|
386
|
+
let closed = false;
|
|
387
|
+
while (index < input.length) {
|
|
388
|
+
const character = input[index];
|
|
389
|
+
index += 1;
|
|
390
|
+
if (character === '"') {
|
|
391
|
+
closed = true;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
if (character === "\\") {
|
|
395
|
+
if (index >= input.length) {
|
|
396
|
+
throw mppError("invalid_header", "MPP quoted-pair is truncated.");
|
|
397
|
+
}
|
|
398
|
+
parsed += input[index];
|
|
399
|
+
index += 1;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
parsed += character;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (!closed)
|
|
406
|
+
throw mppError("invalid_header", "MPP auth-param quote is not closed.");
|
|
407
|
+
if (Object.hasOwn(parameters, name)) {
|
|
408
|
+
throw mppError("invalid_header", `MPP challenge repeats ${name}.`);
|
|
409
|
+
}
|
|
410
|
+
parameters[name] = parsed;
|
|
411
|
+
while (input[index] === " " || input[index] === "\t")
|
|
412
|
+
index += 1;
|
|
413
|
+
if (index < input.length && input[index] !== ",") {
|
|
414
|
+
throw mppError("invalid_header", "MPP challenge auth-params must be comma separated.");
|
|
415
|
+
}
|
|
416
|
+
if (input[index] === ",") {
|
|
417
|
+
index += 1;
|
|
418
|
+
if (index >= input.length) {
|
|
419
|
+
throw mppError("invalid_header", "MPP challenge must not end with a comma.");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const supported = Object.fromEntries(Object.entries(parameters).filter(([name]) => [
|
|
424
|
+
"id",
|
|
425
|
+
"realm",
|
|
426
|
+
"method",
|
|
427
|
+
"intent",
|
|
428
|
+
"request",
|
|
429
|
+
"expires",
|
|
430
|
+
"digest",
|
|
431
|
+
"header",
|
|
432
|
+
"description",
|
|
433
|
+
].includes(name)));
|
|
434
|
+
return normalizeChallenge(supported);
|
|
435
|
+
}
|
|
436
|
+
export async function createNayoriMppUsdcStacksChallenge(input) {
|
|
437
|
+
await createNayoriX402PaymentRequirements(input.quote);
|
|
438
|
+
if (input.quote.paymentAsset !== "usdcx") {
|
|
439
|
+
throw mppError("unsupported_asset", "MPP method=usdc on Stacks requires a USDCx quote.");
|
|
440
|
+
}
|
|
441
|
+
const network = fromStacksX402Network(input.quote.network);
|
|
442
|
+
const definition = getNayoriX402Asset(network, "usdcx");
|
|
443
|
+
const [contractAddress, contractName] = definition.contract.split(".");
|
|
444
|
+
if (!contractAddress || !contractName || !definition.tokenName) {
|
|
445
|
+
throw mppError("invalid_registry", "The canonical USDCx registry entry is invalid.");
|
|
446
|
+
}
|
|
447
|
+
const paymentRequest = Object.freeze({
|
|
448
|
+
amount: input.quote.amount,
|
|
449
|
+
currency: `${definition.contract}::${definition.tokenName}`,
|
|
450
|
+
recipient: input.quote.payTo,
|
|
451
|
+
...(input.description ? { description: input.description } : {}),
|
|
452
|
+
externalId: input.quote.quoteId,
|
|
453
|
+
methodDetails: Object.freeze({
|
|
454
|
+
type: NAYORI_MPP_PROFILE,
|
|
455
|
+
stacks: Object.freeze({
|
|
456
|
+
network,
|
|
457
|
+
chainId: network === "mainnet" ? "1" : "2147483648",
|
|
458
|
+
contractAddress,
|
|
459
|
+
contractName,
|
|
460
|
+
assetName: "usdcx-token",
|
|
461
|
+
functionName: "transfer",
|
|
462
|
+
decimals: 6,
|
|
463
|
+
feePayer: false,
|
|
464
|
+
}),
|
|
465
|
+
}),
|
|
466
|
+
});
|
|
467
|
+
const expiresAtMilliseconds = input.quote.expiresAt * 1_000;
|
|
468
|
+
if (!Number.isFinite(expiresAtMilliseconds) || expiresAtMilliseconds > 8.64e15) {
|
|
469
|
+
throw mppError("invalid_quote", "The trusted quote expiry is outside the RFC3339 range.");
|
|
470
|
+
}
|
|
471
|
+
const challenge = normalizeChallenge({
|
|
472
|
+
id: input.quote.quoteId,
|
|
473
|
+
realm: requireString(input.realm, "realm", { max: 255 }),
|
|
474
|
+
method: NAYORI_MPP_METHOD,
|
|
475
|
+
intent: NAYORI_MPP_INTENT,
|
|
476
|
+
request: encodeNayoriMppJson(paymentRequest),
|
|
477
|
+
expires: new Date(expiresAtMilliseconds).toISOString(),
|
|
478
|
+
digest: digestFromSha256Hex(input.quote.bodySha256),
|
|
479
|
+
header: NAYORI_MPP_CREDENTIAL_HEADER,
|
|
480
|
+
...(input.description ? { description: input.description } : {}),
|
|
481
|
+
});
|
|
482
|
+
return Object.freeze({
|
|
483
|
+
challenge,
|
|
484
|
+
paymentRequest,
|
|
485
|
+
wwwAuthenticate: encodeNayoriMppChallengeHeader(challenge),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
function normalizeHexTransaction(value) {
|
|
489
|
+
const transaction = requireString(value, "transaction", { max: MAX_TRANSACTION_BYTES * 2 + 2 });
|
|
490
|
+
const hex = transaction.startsWith("0x") ? transaction.slice(2) : transaction;
|
|
491
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) {
|
|
492
|
+
throw mppError("invalid_transaction", "Stacks transaction must be hexadecimal consensus bytes.");
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
const decoded = deserializeTransaction(hex);
|
|
496
|
+
if (transactionToHex(decoded).toLowerCase() !== hex.toLowerCase()) {
|
|
497
|
+
throw mppError("invalid_transaction", "Stacks transaction is non-canonical or has trailing bytes.");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch (cause) {
|
|
501
|
+
if (cause instanceof NayoriMppVerificationError)
|
|
502
|
+
throw cause;
|
|
503
|
+
throw mppError("invalid_transaction", "Stacks transaction cannot be decoded.", { cause });
|
|
504
|
+
}
|
|
505
|
+
return hex.toLowerCase();
|
|
506
|
+
}
|
|
507
|
+
function transactionBase64FromHex(value) {
|
|
508
|
+
const hex = normalizeHexTransaction(value);
|
|
509
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
510
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
511
|
+
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
512
|
+
}
|
|
513
|
+
return btoa(bytesToBinary(bytes));
|
|
514
|
+
}
|
|
515
|
+
function transactionHexFromBase64(value) {
|
|
516
|
+
const base64 = requireString(value, "credential.payload.transaction", {
|
|
517
|
+
max: Math.ceil(MAX_TRANSACTION_BYTES / 3) * 4,
|
|
518
|
+
pattern: BASE64_PATTERN,
|
|
519
|
+
});
|
|
520
|
+
if (base64.length % 4 !== 0) {
|
|
521
|
+
throw mppError("invalid_transaction", "MPP Stacks transaction must use padded canonical base64.");
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
const bytes = binaryToBytes(atob(base64));
|
|
525
|
+
if (btoa(bytesToBinary(bytes)) !== base64 || bytes.length > MAX_TRANSACTION_BYTES) {
|
|
526
|
+
throw mppError("invalid_transaction", "MPP Stacks transaction base64 is non-canonical.");
|
|
527
|
+
}
|
|
528
|
+
let hex = "";
|
|
529
|
+
for (const byte of bytes)
|
|
530
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
531
|
+
return normalizeHexTransaction(hex);
|
|
532
|
+
}
|
|
533
|
+
catch (cause) {
|
|
534
|
+
if (cause instanceof NayoriMppVerificationError)
|
|
535
|
+
throw cause;
|
|
536
|
+
throw mppError("invalid_transaction", "MPP Stacks transaction cannot be decoded.", { cause });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function parseSource(value) {
|
|
540
|
+
const source = requireString(value, "credential.source", { max: 96 });
|
|
541
|
+
const match = /^stacks:(1|2147483648):([^.:]+)$/.exec(source);
|
|
542
|
+
if (!match?.[1] || !match[2] || !validateStacksAddress(match[2])) {
|
|
543
|
+
throw mppError("invalid_source", "MPP Stacks source must be stacks:<chainId>:<standard-principal>.");
|
|
544
|
+
}
|
|
545
|
+
const matchesNetwork = match[1] === "1"
|
|
546
|
+
? match[2].startsWith("SP") || match[2].startsWith("SM")
|
|
547
|
+
: match[2].startsWith("ST") || match[2].startsWith("SN");
|
|
548
|
+
if (!matchesNetwork) {
|
|
549
|
+
throw mppError("network_mismatch", "MPP source principal does not match its Stacks chain id.");
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
source,
|
|
553
|
+
chainId: match[1],
|
|
554
|
+
principal: match[2],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function normalizeCredential(value) {
|
|
558
|
+
if (!isRecord(value))
|
|
559
|
+
throw mppError("malformed_credential", "MPP credential must be an object.");
|
|
560
|
+
exactKeys(value, ["challenge", "source", "payload"], "credential");
|
|
561
|
+
const challenge = normalizeChallenge(value.challenge);
|
|
562
|
+
const source = parseSource(value.source).source;
|
|
563
|
+
if (!isRecord(value.payload)) {
|
|
564
|
+
throw mppError("malformed_credential", "MPP credential.payload must be an object.");
|
|
565
|
+
}
|
|
566
|
+
exactKeys(value.payload, ["type", "transaction", "transactionFormat"], "credential.payload");
|
|
567
|
+
if (value.payload.type !== "transaction" ||
|
|
568
|
+
(value.payload.transactionFormat !== undefined &&
|
|
569
|
+
value.payload.transactionFormat !== NAYORI_MPP_TRANSACTION_FORMAT)) {
|
|
570
|
+
throw mppError("unsupported_payload", "MPP Stacks payload requires transaction and stacks_transaction_v1.");
|
|
571
|
+
}
|
|
572
|
+
const transaction = requireString(value.payload.transaction, "credential.payload.transaction", {
|
|
573
|
+
max: Math.ceil(MAX_TRANSACTION_BYTES / 3) * 4,
|
|
574
|
+
pattern: BASE64_PATTERN,
|
|
575
|
+
});
|
|
576
|
+
transactionHexFromBase64(transaction);
|
|
577
|
+
return Object.freeze({
|
|
578
|
+
challenge,
|
|
579
|
+
source,
|
|
580
|
+
payload: Object.freeze({
|
|
581
|
+
type: "transaction",
|
|
582
|
+
transaction,
|
|
583
|
+
...(value.payload.transactionFormat === undefined
|
|
584
|
+
? {}
|
|
585
|
+
: { transactionFormat: NAYORI_MPP_TRANSACTION_FORMAT }),
|
|
586
|
+
}),
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
export function createNayoriMppUsdcStacksCredential(input) {
|
|
590
|
+
const challenge = normalizeChallenge(input.challenge);
|
|
591
|
+
const source = parseSource(input.source);
|
|
592
|
+
const paymentRequest = decodeNayoriMppUsdcStacksRequest(challenge.request);
|
|
593
|
+
if (source.chainId !== paymentRequest.methodDetails.stacks.chainId) {
|
|
594
|
+
throw mppError("network_mismatch", "MPP source chain does not match the payment request.");
|
|
595
|
+
}
|
|
596
|
+
return normalizeCredential({
|
|
597
|
+
challenge,
|
|
598
|
+
source: source.source,
|
|
599
|
+
payload: {
|
|
600
|
+
type: "transaction",
|
|
601
|
+
transaction: transactionBase64FromHex(input.transaction),
|
|
602
|
+
transactionFormat: NAYORI_MPP_TRANSACTION_FORMAT,
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Builds the canonical Nayori USDCx transfer and selects the OnChainOnly
|
|
608
|
+
* anchor mode required by the MPP USDC Stacks profile. The returned
|
|
609
|
+
* transaction is unsigned and can be passed to Leather or another signer.
|
|
610
|
+
*/
|
|
611
|
+
export async function buildNayoriMppUnsignedPaymentTransaction(intent) {
|
|
612
|
+
if (intent.asset !== "usdcx") {
|
|
613
|
+
throw mppError("unsupported_asset", "MPP method=usdc on Stacks requires a USDCx intent.");
|
|
614
|
+
}
|
|
615
|
+
const transaction = deserializeTransaction(await buildNayoriX402UnsignedPaymentTransaction(intent));
|
|
616
|
+
transaction.anchorMode = AnchorMode.OnChainOnly;
|
|
617
|
+
return transactionToHex(transaction).toLowerCase();
|
|
618
|
+
}
|
|
619
|
+
export function encodeNayoriMppCredentialHeader(credentialInput) {
|
|
620
|
+
const credential = normalizeCredential(credentialInput);
|
|
621
|
+
return `${MPP_PAYMENT_SCHEME} ${encodeNayoriMppJson(credential)}`;
|
|
622
|
+
}
|
|
623
|
+
export function decodeNayoriMppCredentialHeader(value) {
|
|
624
|
+
const match = /^Payment ([A-Za-z0-9_-]+)$/.exec(value);
|
|
625
|
+
if (!match?.[1]) {
|
|
626
|
+
throw mppError("malformed_credential", "Payment credential header is malformed.");
|
|
627
|
+
}
|
|
628
|
+
return normalizeCredential(decodeNayoriMppJson(match[1], "MPP credential"));
|
|
629
|
+
}
|
|
630
|
+
function challengesEqual(left, right) {
|
|
631
|
+
return canonicalizeNayoriMppJson(left) === canonicalizeNayoriMppJson(right);
|
|
632
|
+
}
|
|
633
|
+
function ensureLowSStandardOnChainTransaction(transactionHex) {
|
|
634
|
+
const transaction = deserializeTransaction(transactionHex);
|
|
635
|
+
if (transaction.anchorMode !== AnchorMode.OnChainOnly) {
|
|
636
|
+
throw mppError("anchor_mode_mismatch", "MPP Stacks transactions require OnChainOnly anchor mode.");
|
|
637
|
+
}
|
|
638
|
+
if (transaction.auth.authType !== AuthType.Standard) {
|
|
639
|
+
throw mppError("sponsorship_not_enabled", "This MPP profile accepts standard transactions only.");
|
|
640
|
+
}
|
|
641
|
+
const condition = transaction.auth.spendingCondition;
|
|
642
|
+
if (!isSingleSig(condition)) {
|
|
643
|
+
throw mppError("unsupported_authorization", "MPP Stacks requires a single origin signature.");
|
|
644
|
+
}
|
|
645
|
+
const signature = condition.signature.data;
|
|
646
|
+
if (!/^(?:0[0-3])[0-9a-f]{128}$/i.test(signature)) {
|
|
647
|
+
throw mppError("invalid_origin_signature", "MPP Stacks origin signature encoding is invalid.");
|
|
648
|
+
}
|
|
649
|
+
const s = BigInt(`0x${signature.slice(66)}`);
|
|
650
|
+
if (s === 0n || s > SECP256K1_HALF_ORDER) {
|
|
651
|
+
throw mppError("non_canonical_signature", "MPP Stacks origin signature must use low-s form.");
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
export async function verifyNayoriMppUsdcStacksPayment(input) {
|
|
655
|
+
const credential = normalizeCredential(input.credential);
|
|
656
|
+
const expectedChallenge = normalizeChallenge(input.expectedChallenge);
|
|
657
|
+
if (!challengesEqual(credential.challenge, expectedChallenge)) {
|
|
658
|
+
throw mppError("invalid_challenge", "MPP credential does not echo the selected challenge exactly.");
|
|
659
|
+
}
|
|
660
|
+
const expectedBundle = await createNayoriMppUsdcStacksChallenge({
|
|
661
|
+
quote: input.trustedQuote,
|
|
662
|
+
realm: expectedChallenge.realm,
|
|
663
|
+
...(expectedChallenge.description ? { description: expectedChallenge.description } : {}),
|
|
664
|
+
});
|
|
665
|
+
if (!challengesEqual(expectedBundle.challenge, expectedChallenge)) {
|
|
666
|
+
throw mppError("invalid_challenge", "MPP challenge is not bound to the trusted Nayori quote.");
|
|
667
|
+
}
|
|
668
|
+
const nowSeconds = input.nowSeconds ?? Math.floor(Date.now() / 1_000);
|
|
669
|
+
const clockSkewSeconds = input.clockSkewSeconds ?? 30;
|
|
670
|
+
if (!Number.isSafeInteger(nowSeconds) ||
|
|
671
|
+
nowSeconds < 0 ||
|
|
672
|
+
!Number.isSafeInteger(clockSkewSeconds) ||
|
|
673
|
+
clockSkewSeconds < 0) {
|
|
674
|
+
throw mppError("invalid_verifier_config", "MPP verifier timestamps are invalid.");
|
|
675
|
+
}
|
|
676
|
+
if (nowSeconds * 1_000 > Date.parse(expectedChallenge.expires) + clockSkewSeconds * 1_000) {
|
|
677
|
+
throw mppError("payment_expired", "MPP challenge has expired.");
|
|
678
|
+
}
|
|
679
|
+
const paymentRequest = decodeNayoriMppUsdcStacksRequest(expectedChallenge.request);
|
|
680
|
+
const source = parseSource(credential.source);
|
|
681
|
+
if (source.chainId !== paymentRequest.methodDetails.stacks.chainId) {
|
|
682
|
+
throw mppError("network_mismatch", "MPP payer source is on the wrong Stacks chain.");
|
|
683
|
+
}
|
|
684
|
+
const transaction = transactionHexFromBase64(credential.payload.transaction);
|
|
685
|
+
ensureLowSStandardOnChainTransaction(transaction);
|
|
686
|
+
const requirements = await createNayoriX402PaymentRequirements(input.trustedQuote);
|
|
687
|
+
const verified = await verifyNayoriX402DirectPayment({
|
|
688
|
+
paymentRequirements: requirements,
|
|
689
|
+
paymentPayload: createNayoriX402DirectPaymentPayload({
|
|
690
|
+
paymentRequirements: requirements,
|
|
691
|
+
transaction,
|
|
692
|
+
resource: { url: input.trustedQuote.url },
|
|
693
|
+
}),
|
|
694
|
+
trustedQuote: input.trustedQuote,
|
|
695
|
+
request: input.request,
|
|
696
|
+
nowSeconds,
|
|
697
|
+
clockSkewSeconds,
|
|
698
|
+
});
|
|
699
|
+
if (verified.asset !== "usdcx" || verified.payer !== source.principal) {
|
|
700
|
+
throw mppError("payer_mismatch", "MPP source does not match the signed USDCx transfer origin.");
|
|
701
|
+
}
|
|
702
|
+
if (paymentRequest.amount !== verified.amount.toString() ||
|
|
703
|
+
paymentRequest.recipient !== verified.payTo ||
|
|
704
|
+
paymentRequest.externalId !== verified.quoteId) {
|
|
705
|
+
throw mppError("payment_request_mismatch", "MPP request does not match the verified payment.");
|
|
706
|
+
}
|
|
707
|
+
return Object.freeze({
|
|
708
|
+
...verified,
|
|
709
|
+
protocol: "mpp",
|
|
710
|
+
method: NAYORI_MPP_METHOD,
|
|
711
|
+
intent: NAYORI_MPP_INTENT,
|
|
712
|
+
profile: NAYORI_MPP_PROFILE,
|
|
713
|
+
challengeId: expectedChallenge.id,
|
|
714
|
+
source: source.source,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
function normalizeReceiptReference(value) {
|
|
718
|
+
try {
|
|
719
|
+
return normalizeTxid(requireString(value, "receipt.reference", { max: 128 }));
|
|
720
|
+
}
|
|
721
|
+
catch (cause) {
|
|
722
|
+
throw mppError("invalid_receipt", "MPP Stacks receipt reference must be a transaction ID.", {
|
|
723
|
+
cause,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
export function createNayoriMppUsdcStacksReceipt(input) {
|
|
728
|
+
if (input.network !== "mainnet" && input.network !== "testnet") {
|
|
729
|
+
throw mppError("invalid_receipt", "Receipt network must be mainnet or testnet.");
|
|
730
|
+
}
|
|
731
|
+
const settledAt = input.settledAt ?? new Date();
|
|
732
|
+
const timestamp = settledAt instanceof Date
|
|
733
|
+
? settledAt.toISOString()
|
|
734
|
+
: normalizeRfc3339(settledAt, "settledAt");
|
|
735
|
+
return Object.freeze({
|
|
736
|
+
method: NAYORI_MPP_METHOD,
|
|
737
|
+
type: NAYORI_MPP_PROFILE,
|
|
738
|
+
challengeId: requireString(input.challengeId, "challengeId", {
|
|
739
|
+
max: 128,
|
|
740
|
+
pattern: CHALLENGE_ID_PATTERN,
|
|
741
|
+
}),
|
|
742
|
+
reference: normalizeReceiptReference(input.reference),
|
|
743
|
+
status: "success",
|
|
744
|
+
timestamp,
|
|
745
|
+
network: input.network === "mainnet" ? "stacks:1" : "stacks:2147483648",
|
|
746
|
+
...(input.externalId
|
|
747
|
+
? { externalId: requireString(input.externalId, "externalId", { max: 128 }) }
|
|
748
|
+
: {}),
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
export function encodeNayoriMppReceiptHeader(receipt) {
|
|
752
|
+
return encodeNayoriMppJson(receipt);
|
|
753
|
+
}
|
|
754
|
+
export function decodeNayoriMppReceiptHeader(value) {
|
|
755
|
+
const decoded = decodeNayoriMppJson(value, "Payment-Receipt");
|
|
756
|
+
if (!isRecord(decoded))
|
|
757
|
+
throw mppError("invalid_receipt", "Payment-Receipt must be an object.");
|
|
758
|
+
exactKeys(decoded, ["method", "type", "challengeId", "reference", "status", "timestamp", "network", "externalId"], "Payment-Receipt");
|
|
759
|
+
if (decoded.method !== NAYORI_MPP_METHOD ||
|
|
760
|
+
decoded.type !== NAYORI_MPP_PROFILE ||
|
|
761
|
+
decoded.status !== "success" ||
|
|
762
|
+
(decoded.network !== "stacks:1" && decoded.network !== "stacks:2147483648")) {
|
|
763
|
+
throw mppError("invalid_receipt", "Payment-Receipt profile fields are invalid.");
|
|
764
|
+
}
|
|
765
|
+
return Object.freeze({
|
|
766
|
+
method: NAYORI_MPP_METHOD,
|
|
767
|
+
type: NAYORI_MPP_PROFILE,
|
|
768
|
+
challengeId: requireString(decoded.challengeId, "receipt.challengeId", {
|
|
769
|
+
max: 128,
|
|
770
|
+
pattern: CHALLENGE_ID_PATTERN,
|
|
771
|
+
}),
|
|
772
|
+
reference: normalizeReceiptReference(decoded.reference),
|
|
773
|
+
status: "success",
|
|
774
|
+
timestamp: normalizeRfc3339(decoded.timestamp, "receipt.timestamp"),
|
|
775
|
+
network: decoded.network,
|
|
776
|
+
...(decoded.externalId === undefined
|
|
777
|
+
? {}
|
|
778
|
+
: { externalId: requireString(decoded.externalId, "receipt.externalId", { max: 128 }) }),
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
export function nayoriMppStacksReplayKey(payment) {
|
|
782
|
+
return `mpp:${payment.network}:${payment.payer}:${payment.originNonce.toString()}:${payment.transactionHash}`;
|
|
783
|
+
}
|
|
784
|
+
//# sourceMappingURL=mpp.js.map
|