@tamtamchik/app-store-receipt-parser 1.0.1 → 2.1.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/README.md +46 -20
- package/dist/index.js +155 -67
- package/dist/index.mjs +155 -67
- package/package.json +9 -9
- package/src/constants.ts +11 -0
- package/src/index.ts +2 -1
- package/src/mappings.ts +62 -0
- package/src/parser.ts +114 -0
- package/src/utils.ts +1 -0
- package/src/verifications.ts +62 -0
- package/src/ReceiptParser.ts +0 -109
package/README.md
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# Apple Receipt Parser
|
|
2
2
|
|
|
3
|
-
[![Buy Me A Coffee][ico-coffee]][link-coffee]
|
|
4
3
|
[![Latest Version on NPM][ico-version]][link-npm]
|
|
5
4
|
[![Scrutinizer build][ico-scrutinizer-build]][link-scrutinizer]
|
|
6
5
|
[![Scrutinizer quality][ico-scrutinizer-quality]][link-scrutinizer]
|
|
@@ -11,8 +10,10 @@
|
|
|
11
10
|
A lightweight TypeScript library for extracting transaction IDs from Apple's ASN.1 encoded Unified Receipts.
|
|
12
11
|
|
|
13
12
|
> **Warning!** This library is not a full-fledged receipt parser.
|
|
14
|
-
> It only extracts
|
|
15
|
-
> It does not work with the old
|
|
13
|
+
> It only extracts some information from Apple's ASN.1 encoded Unified Receipts.
|
|
14
|
+
> It does not work with the old-style transactions receipts.
|
|
15
|
+
|
|
16
|
+
> **Documentation for the version 1.x of the library can be found [here](https://github.com/tamtamchik/app-store-receipt-parser/tree/1.x/README.md).**
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
@@ -31,29 +32,51 @@ yarn add @tamtamchik/app-store-receipt-parser
|
|
|
31
32
|
## Usage
|
|
32
33
|
|
|
33
34
|
```typescript
|
|
34
|
-
import {
|
|
35
|
+
import { parseReceipt } from '@tamtamchik/app-store-receipt-parser';
|
|
35
36
|
|
|
36
37
|
// Unified Receipt string
|
|
37
38
|
const receiptString = "MII...";
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
39
|
+
const data = parseReceipt(receiptString);
|
|
40
|
+
|
|
41
|
+
console.log(data);
|
|
42
|
+
// {
|
|
43
|
+
// ENVIRONMENT: 'ProductionSandbox',
|
|
44
|
+
// APP_VERSION: '1',
|
|
45
|
+
// ORIGINAL_APP_VERSION: '1.0',
|
|
46
|
+
// OPAQUE_VALUE: 'c4dd4054b0b61a07beb585f6a842e048',
|
|
47
|
+
// SHA1_HASH: '2e0a115beac1c57023a5bd37349955a9ad99db4d',
|
|
48
|
+
// BUNDLE_ID: 'com.mbaasy.ios.demo',
|
|
49
|
+
// RECEIPT_CREATION_DATE: '2015-08-13T07:50:46Z',
|
|
50
|
+
// ORIGINAL_PURCHASE_DATE: '2013-08-01T07:00:00Z',
|
|
51
|
+
// IN_APP_EXPIRES_DATE: '2015-08-10T07:19:32Z',
|
|
52
|
+
// IN_APP_CANCELLATION_DATE: '',
|
|
53
|
+
// IN_APP_QUANTITY: '020101',
|
|
54
|
+
// IN_APP_WEB_ORDER_LINE_ITEM_ID: '0207038d7ea69472c9',
|
|
55
|
+
// IN_APP_PRODUCT_ID: 'monthly',
|
|
56
|
+
// IN_APP_TRANSACTION_ID: '1000000166967782',
|
|
57
|
+
// IN_APP_TRANSACTION_IDS: [
|
|
58
|
+
// '1000000166865231',
|
|
59
|
+
// '1000000166965150',
|
|
60
|
+
// '1000000166965327',
|
|
61
|
+
// '1000000166965895',
|
|
62
|
+
// '1000000166967152',
|
|
63
|
+
// '1000000166967484',
|
|
64
|
+
// '1000000166967782'
|
|
65
|
+
// ],
|
|
66
|
+
// IN_APP_ORIGINAL_TRANSACTION_ID: '1000000166965150',
|
|
67
|
+
// IN_APP_ORIGINAL_TRANSACTION_IDS: [
|
|
68
|
+
// '1000000166865231',
|
|
69
|
+
// '1000000166965150'
|
|
70
|
+
// ],
|
|
71
|
+
// IN_APP_PURCHASE_DATE: '2015-08-10T07:14:32Z',
|
|
72
|
+
// IN_APP_ORIGINAL_PURCHASE_DATE: '2015-08-10T07:12:34Z'
|
|
54
73
|
// }
|
|
55
74
|
```
|
|
56
75
|
|
|
76
|
+
## Special Thanks
|
|
77
|
+
|
|
78
|
+
- [@Jurajzovinec](https://github.com/Jurajzovinec) for his superb contribution to the project.
|
|
79
|
+
|
|
57
80
|
## Contributing
|
|
58
81
|
|
|
59
82
|
Pull requests are always welcome. If you have bigger changes in mind, please open an issue first to discuss your ideas.
|
|
@@ -66,6 +89,9 @@ Apple Receipt Parser is [MIT licensed](./LICENSE).
|
|
|
66
89
|
|
|
67
90
|
This project uses `ASN1.js`, which is licensed under the BSD-3-Clause License. The license text can be found in [LICENSE](./LICENSE).
|
|
68
91
|
|
|
92
|
+
---
|
|
93
|
+
[![Buy Me A Coffee][ico-coffee]][link-coffee]
|
|
94
|
+
|
|
69
95
|
[ico-coffee]: https://img.shields.io/badge/Buy%20Me%20A-Coffee-%236F4E37.svg?style=flat-square
|
|
70
96
|
[ico-version]: https://img.shields.io/npm/v/@tamtamchik/app-store-receipt-parser.svg?style=flat-square
|
|
71
97
|
[ico-license]: https://img.shields.io/npm/l/@tamtamchik/app-store-receipt-parser.svg?style=flat-square
|
package/dist/index.js
CHANGED
|
@@ -20,87 +20,175 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
-
|
|
23
|
+
parseReceipt: () => parseReceipt
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(src_exports);
|
|
26
26
|
|
|
27
|
-
// src/
|
|
27
|
+
// src/parser.ts
|
|
28
|
+
var import_asn1js2 = require("asn1js");
|
|
29
|
+
|
|
30
|
+
// src/mappings.ts
|
|
31
|
+
var RECEIPT_FIELDS_MAP = /* @__PURE__ */ new Map([
|
|
32
|
+
[0, "ENVIRONMENT"],
|
|
33
|
+
[2, "BUNDLE_ID"],
|
|
34
|
+
[3, "APP_VERSION"],
|
|
35
|
+
[4, "OPAQUE_VALUE"],
|
|
36
|
+
[5, "SHA1_HASH"],
|
|
37
|
+
[12, "RECEIPT_CREATION_DATE"],
|
|
38
|
+
[18, "ORIGINAL_PURCHASE_DATE"],
|
|
39
|
+
[19, "ORIGINAL_APP_VERSION"],
|
|
40
|
+
[1701, "IN_APP_QUANTITY"],
|
|
41
|
+
[1702, "IN_APP_PRODUCT_ID"],
|
|
42
|
+
[1703, "IN_APP_TRANSACTION_ID"],
|
|
43
|
+
[1704, "IN_APP_PURCHASE_DATE"],
|
|
44
|
+
[1705, "IN_APP_ORIGINAL_TRANSACTION_ID"],
|
|
45
|
+
[1706, "IN_APP_ORIGINAL_PURCHASE_DATE"],
|
|
46
|
+
[1708, "IN_APP_EXPIRES_DATE"],
|
|
47
|
+
[1711, "IN_APP_WEB_ORDER_LINE_ITEM_ID"],
|
|
48
|
+
[1712, "IN_APP_CANCELLATION_DATE"]
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// src/constants.ts
|
|
52
|
+
var IN_APP = 17;
|
|
53
|
+
var CONTENT_ID = "pkcs7_content";
|
|
54
|
+
var FIELD_TYPE_ID = "FieldType";
|
|
55
|
+
var FIELD_VALUE_ID = "FieldTypeOctetString";
|
|
56
|
+
|
|
57
|
+
// src/verifications.ts
|
|
28
58
|
var import_asn1js = require("asn1js");
|
|
29
|
-
var
|
|
30
|
-
|
|
31
|
-
|
|
59
|
+
var receiptSchema = new import_asn1js.Sequence({
|
|
60
|
+
value: [
|
|
61
|
+
new import_asn1js.ObjectIdentifier(),
|
|
62
|
+
new import_asn1js.Constructed({
|
|
63
|
+
idBlock: { tagClass: 3, tagNumber: 0 },
|
|
64
|
+
value: [
|
|
65
|
+
new import_asn1js.Sequence({
|
|
66
|
+
value: [
|
|
67
|
+
new import_asn1js.Integer(),
|
|
68
|
+
new import_asn1js.Set({
|
|
69
|
+
value: [
|
|
70
|
+
new import_asn1js.Sequence({
|
|
71
|
+
value: [new import_asn1js.ObjectIdentifier(), new import_asn1js.Any()]
|
|
72
|
+
})
|
|
73
|
+
]
|
|
74
|
+
}),
|
|
75
|
+
new import_asn1js.Sequence({
|
|
76
|
+
value: [
|
|
77
|
+
new import_asn1js.ObjectIdentifier(),
|
|
78
|
+
new import_asn1js.Constructed({
|
|
79
|
+
idBlock: { tagClass: 3, tagNumber: 0 },
|
|
80
|
+
value: [new import_asn1js.OctetString({ name: CONTENT_ID })]
|
|
81
|
+
})
|
|
82
|
+
]
|
|
83
|
+
})
|
|
84
|
+
]
|
|
85
|
+
})
|
|
86
|
+
]
|
|
87
|
+
})
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
var fieldSchema = new import_asn1js.Sequence({
|
|
91
|
+
value: [
|
|
92
|
+
new import_asn1js.Integer({ name: FIELD_TYPE_ID }),
|
|
93
|
+
new import_asn1js.Integer(),
|
|
94
|
+
new import_asn1js.OctetString({ name: FIELD_VALUE_ID })
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
function verifyReceiptSchema(receipt) {
|
|
98
|
+
const receiptVerification = (0, import_asn1js.verifySchema)(Buffer.from(receipt, "base64"), receiptSchema);
|
|
99
|
+
if (!receiptVerification.verified) {
|
|
100
|
+
throw new Error("Receipt verification failed.");
|
|
32
101
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const result2 = this.extractTransactionIds(valueBlock.value);
|
|
40
|
-
if (result2)
|
|
41
|
-
return result2;
|
|
42
|
-
}
|
|
43
|
-
const result = { transactionIds: [], originalTransactionIds: [] };
|
|
44
|
-
for (const innerBlock of valueBlock.value) {
|
|
45
|
-
const innerIds = this.getTransactionIdsFromBlock(innerBlock);
|
|
46
|
-
if (innerIds) {
|
|
47
|
-
result.transactionIds.push(...innerIds.transactionIds);
|
|
48
|
-
result.originalTransactionIds.push(...innerIds.originalTransactionIds);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
result.transactionIds = [...new Set(result.transactionIds)];
|
|
52
|
-
result.originalTransactionIds = [...new Set(result.originalTransactionIds)];
|
|
53
|
-
return result;
|
|
102
|
+
return receiptVerification;
|
|
103
|
+
}
|
|
104
|
+
function verifyFieldSchema(sequence) {
|
|
105
|
+
const fieldVerification = (0, import_asn1js.verifySchema)(sequence.toBER(), fieldSchema);
|
|
106
|
+
if (!fieldVerification.verified) {
|
|
107
|
+
return null;
|
|
54
108
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
109
|
+
return fieldVerification;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/utils.ts
|
|
113
|
+
var uniqueArrayValues = (array) => Array.from(new Set(array));
|
|
114
|
+
|
|
115
|
+
// src/parser.ts
|
|
116
|
+
function isReceiptFieldKey(value) {
|
|
117
|
+
return Boolean(typeof value === "number" && RECEIPT_FIELDS_MAP.has(value));
|
|
118
|
+
}
|
|
119
|
+
function isParsedReceiptContentComplete(data) {
|
|
120
|
+
for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
|
|
121
|
+
if (!(fieldKey in data)) {
|
|
122
|
+
return false;
|
|
59
123
|
}
|
|
60
|
-
return this.getTransactionIdsFromBlock(result.toJSON());
|
|
61
124
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
function extractFieldValue(field) {
|
|
128
|
+
const [fieldValue] = field.valueBlock.value;
|
|
129
|
+
if (fieldValue instanceof import_asn1js2.IA5String || fieldValue instanceof import_asn1js2.Utf8String) {
|
|
130
|
+
return fieldValue.valueBlock.value;
|
|
67
131
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
132
|
+
return field.toJSON().valueBlock.valueHex;
|
|
133
|
+
}
|
|
134
|
+
function appendField(parsed, name, value) {
|
|
135
|
+
if (name === "IN_APP_ORIGINAL_TRANSACTION_ID") {
|
|
136
|
+
parsed.IN_APP_ORIGINAL_TRANSACTION_IDS.push(value);
|
|
73
137
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
if (block.blockName === "OCTET STRING" && block.valueBlock.value[0].blockName === "UTF8String") {
|
|
79
|
-
return block.valueBlock.value[0].valueBlock.value;
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
138
|
+
if (name === "IN_APP_TRANSACTION_ID") {
|
|
139
|
+
parsed.IN_APP_TRANSACTION_IDS.push(value);
|
|
82
140
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
141
|
+
parsed[name] = value;
|
|
142
|
+
}
|
|
143
|
+
function processField(parsed, fieldKey, fieldValue) {
|
|
144
|
+
if (fieldKey === IN_APP) {
|
|
145
|
+
parseOctetStringContent(parsed, fieldValue);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!isReceiptFieldKey(fieldKey)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const name = RECEIPT_FIELDS_MAP.get(fieldKey);
|
|
152
|
+
appendField(parsed, name, extractFieldValue(fieldValue));
|
|
153
|
+
}
|
|
154
|
+
function parseOctetStringContent(parsed, content) {
|
|
155
|
+
const [contentSet] = content.valueBlock.value;
|
|
156
|
+
const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof import_asn1js2.Sequence);
|
|
157
|
+
for (const sequence of contentSetSequences) {
|
|
158
|
+
const verifiedSequence = verifyFieldSchema(sequence);
|
|
159
|
+
if (verifiedSequence) {
|
|
160
|
+
const fieldKey = verifiedSequence.result[FIELD_TYPE_ID].valueBlock.valueDec;
|
|
161
|
+
const fieldValueOctetString = verifiedSequence.result[FIELD_VALUE_ID];
|
|
162
|
+
processField(parsed, fieldKey, fieldValueOctetString);
|
|
96
163
|
}
|
|
97
|
-
return result;
|
|
98
164
|
}
|
|
99
|
-
|
|
100
|
-
|
|
165
|
+
}
|
|
166
|
+
function postprocessParsedReceipt(parsed) {
|
|
167
|
+
parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
|
|
168
|
+
parsed.IN_APP_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_TRANSACTION_IDS);
|
|
169
|
+
}
|
|
170
|
+
function parseReceipt(receipt) {
|
|
171
|
+
const rootSchemaVerification = verifyReceiptSchema(receipt);
|
|
172
|
+
const content = rootSchemaVerification.result[CONTENT_ID];
|
|
173
|
+
const parsed = {
|
|
174
|
+
ENVIRONMENT: "Production",
|
|
175
|
+
IN_APP_ORIGINAL_TRANSACTION_IDS: [],
|
|
176
|
+
IN_APP_TRANSACTION_IDS: []
|
|
177
|
+
};
|
|
178
|
+
parseOctetStringContent(parsed, content);
|
|
179
|
+
if (!isParsedReceiptContentComplete(parsed)) {
|
|
180
|
+
const missingProps = [];
|
|
181
|
+
for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
|
|
182
|
+
if (!(fieldKey in parsed)) {
|
|
183
|
+
missingProps.push(fieldKey);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`Missing required fields: ${missingProps.join(", ")}`);
|
|
101
187
|
}
|
|
102
|
-
|
|
188
|
+
postprocessParsedReceipt(parsed);
|
|
189
|
+
return parsed;
|
|
190
|
+
}
|
|
103
191
|
// Annotate the CommonJS export names for ESM import in node:
|
|
104
192
|
0 && (module.exports = {
|
|
105
|
-
|
|
193
|
+
parseReceipt
|
|
106
194
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,79 +1,167 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// src/parser.ts
|
|
2
|
+
import { IA5String, Sequence as Sequence2, Utf8String } from "asn1js";
|
|
3
|
+
|
|
4
|
+
// src/mappings.ts
|
|
5
|
+
var RECEIPT_FIELDS_MAP = /* @__PURE__ */ new Map([
|
|
6
|
+
[0, "ENVIRONMENT"],
|
|
7
|
+
[2, "BUNDLE_ID"],
|
|
8
|
+
[3, "APP_VERSION"],
|
|
9
|
+
[4, "OPAQUE_VALUE"],
|
|
10
|
+
[5, "SHA1_HASH"],
|
|
11
|
+
[12, "RECEIPT_CREATION_DATE"],
|
|
12
|
+
[18, "ORIGINAL_PURCHASE_DATE"],
|
|
13
|
+
[19, "ORIGINAL_APP_VERSION"],
|
|
14
|
+
[1701, "IN_APP_QUANTITY"],
|
|
15
|
+
[1702, "IN_APP_PRODUCT_ID"],
|
|
16
|
+
[1703, "IN_APP_TRANSACTION_ID"],
|
|
17
|
+
[1704, "IN_APP_PURCHASE_DATE"],
|
|
18
|
+
[1705, "IN_APP_ORIGINAL_TRANSACTION_ID"],
|
|
19
|
+
[1706, "IN_APP_ORIGINAL_PURCHASE_DATE"],
|
|
20
|
+
[1708, "IN_APP_EXPIRES_DATE"],
|
|
21
|
+
[1711, "IN_APP_WEB_ORDER_LINE_ITEM_ID"],
|
|
22
|
+
[1712, "IN_APP_CANCELLATION_DATE"]
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// src/constants.ts
|
|
26
|
+
var IN_APP = 17;
|
|
27
|
+
var CONTENT_ID = "pkcs7_content";
|
|
28
|
+
var FIELD_TYPE_ID = "FieldType";
|
|
29
|
+
var FIELD_VALUE_ID = "FieldTypeOctetString";
|
|
30
|
+
|
|
31
|
+
// src/verifications.ts
|
|
32
|
+
import { Any, Constructed, Integer, ObjectIdentifier, OctetString, Sequence, Set as Set2, verifySchema } from "asn1js";
|
|
33
|
+
var receiptSchema = new Sequence({
|
|
34
|
+
value: [
|
|
35
|
+
new ObjectIdentifier(),
|
|
36
|
+
new Constructed({
|
|
37
|
+
idBlock: { tagClass: 3, tagNumber: 0 },
|
|
38
|
+
value: [
|
|
39
|
+
new Sequence({
|
|
40
|
+
value: [
|
|
41
|
+
new Integer(),
|
|
42
|
+
new Set2({
|
|
43
|
+
value: [
|
|
44
|
+
new Sequence({
|
|
45
|
+
value: [new ObjectIdentifier(), new Any()]
|
|
46
|
+
})
|
|
47
|
+
]
|
|
48
|
+
}),
|
|
49
|
+
new Sequence({
|
|
50
|
+
value: [
|
|
51
|
+
new ObjectIdentifier(),
|
|
52
|
+
new Constructed({
|
|
53
|
+
idBlock: { tagClass: 3, tagNumber: 0 },
|
|
54
|
+
value: [new OctetString({ name: CONTENT_ID })]
|
|
55
|
+
})
|
|
56
|
+
]
|
|
57
|
+
})
|
|
58
|
+
]
|
|
59
|
+
})
|
|
60
|
+
]
|
|
61
|
+
})
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
var fieldSchema = new Sequence({
|
|
65
|
+
value: [
|
|
66
|
+
new Integer({ name: FIELD_TYPE_ID }),
|
|
67
|
+
new Integer(),
|
|
68
|
+
new OctetString({ name: FIELD_VALUE_ID })
|
|
69
|
+
]
|
|
70
|
+
});
|
|
71
|
+
function verifyReceiptSchema(receipt) {
|
|
72
|
+
const receiptVerification = verifySchema(Buffer.from(receipt, "base64"), receiptSchema);
|
|
73
|
+
if (!receiptVerification.verified) {
|
|
74
|
+
throw new Error("Receipt verification failed.");
|
|
6
75
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const result2 = this.extractTransactionIds(valueBlock.value);
|
|
14
|
-
if (result2)
|
|
15
|
-
return result2;
|
|
16
|
-
}
|
|
17
|
-
const result = { transactionIds: [], originalTransactionIds: [] };
|
|
18
|
-
for (const innerBlock of valueBlock.value) {
|
|
19
|
-
const innerIds = this.getTransactionIdsFromBlock(innerBlock);
|
|
20
|
-
if (innerIds) {
|
|
21
|
-
result.transactionIds.push(...innerIds.transactionIds);
|
|
22
|
-
result.originalTransactionIds.push(...innerIds.originalTransactionIds);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
result.transactionIds = [...new Set(result.transactionIds)];
|
|
26
|
-
result.originalTransactionIds = [...new Set(result.originalTransactionIds)];
|
|
27
|
-
return result;
|
|
76
|
+
return receiptVerification;
|
|
77
|
+
}
|
|
78
|
+
function verifyFieldSchema(sequence) {
|
|
79
|
+
const fieldVerification = verifySchema(sequence.toBER(), fieldSchema);
|
|
80
|
+
if (!fieldVerification.verified) {
|
|
81
|
+
return null;
|
|
28
82
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
83
|
+
return fieldVerification;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/utils.ts
|
|
87
|
+
var uniqueArrayValues = (array) => Array.from(new Set(array));
|
|
88
|
+
|
|
89
|
+
// src/parser.ts
|
|
90
|
+
function isReceiptFieldKey(value) {
|
|
91
|
+
return Boolean(typeof value === "number" && RECEIPT_FIELDS_MAP.has(value));
|
|
92
|
+
}
|
|
93
|
+
function isParsedReceiptContentComplete(data) {
|
|
94
|
+
for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
|
|
95
|
+
if (!(fieldKey in data)) {
|
|
96
|
+
return false;
|
|
33
97
|
}
|
|
34
|
-
return this.getTransactionIdsFromBlock(result.toJSON());
|
|
35
98
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
function extractFieldValue(field) {
|
|
102
|
+
const [fieldValue] = field.valueBlock.value;
|
|
103
|
+
if (fieldValue instanceof IA5String || fieldValue instanceof Utf8String) {
|
|
104
|
+
return fieldValue.valueBlock.value;
|
|
41
105
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
106
|
+
return field.toJSON().valueBlock.valueHex;
|
|
107
|
+
}
|
|
108
|
+
function appendField(parsed, name, value) {
|
|
109
|
+
if (name === "IN_APP_ORIGINAL_TRANSACTION_ID") {
|
|
110
|
+
parsed.IN_APP_ORIGINAL_TRANSACTION_IDS.push(value);
|
|
47
111
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
112
|
+
if (name === "IN_APP_TRANSACTION_ID") {
|
|
113
|
+
parsed.IN_APP_TRANSACTION_IDS.push(value);
|
|
114
|
+
}
|
|
115
|
+
parsed[name] = value;
|
|
116
|
+
}
|
|
117
|
+
function processField(parsed, fieldKey, fieldValue) {
|
|
118
|
+
if (fieldKey === IN_APP) {
|
|
119
|
+
parseOctetStringContent(parsed, fieldValue);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!isReceiptFieldKey(fieldKey)) {
|
|
123
|
+
return;
|
|
56
124
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return null;
|
|
125
|
+
const name = RECEIPT_FIELDS_MAP.get(fieldKey);
|
|
126
|
+
appendField(parsed, name, extractFieldValue(fieldValue));
|
|
127
|
+
}
|
|
128
|
+
function parseOctetStringContent(parsed, content) {
|
|
129
|
+
const [contentSet] = content.valueBlock.value;
|
|
130
|
+
const contentSetSequences = contentSet.valueBlock.value.filter((v) => v instanceof Sequence2);
|
|
131
|
+
for (const sequence of contentSetSequences) {
|
|
132
|
+
const verifiedSequence = verifyFieldSchema(sequence);
|
|
133
|
+
if (verifiedSequence) {
|
|
134
|
+
const fieldKey = verifiedSequence.result[FIELD_TYPE_ID].valueBlock.valueDec;
|
|
135
|
+
const fieldValueOctetString = verifiedSequence.result[FIELD_VALUE_ID];
|
|
136
|
+
processField(parsed, fieldKey, fieldValueOctetString);
|
|
70
137
|
}
|
|
71
|
-
return result;
|
|
72
138
|
}
|
|
73
|
-
|
|
74
|
-
|
|
139
|
+
}
|
|
140
|
+
function postprocessParsedReceipt(parsed) {
|
|
141
|
+
parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_ORIGINAL_TRANSACTION_IDS);
|
|
142
|
+
parsed.IN_APP_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_TRANSACTION_IDS);
|
|
143
|
+
}
|
|
144
|
+
function parseReceipt(receipt) {
|
|
145
|
+
const rootSchemaVerification = verifyReceiptSchema(receipt);
|
|
146
|
+
const content = rootSchemaVerification.result[CONTENT_ID];
|
|
147
|
+
const parsed = {
|
|
148
|
+
ENVIRONMENT: "Production",
|
|
149
|
+
IN_APP_ORIGINAL_TRANSACTION_IDS: [],
|
|
150
|
+
IN_APP_TRANSACTION_IDS: []
|
|
151
|
+
};
|
|
152
|
+
parseOctetStringContent(parsed, content);
|
|
153
|
+
if (!isParsedReceiptContentComplete(parsed)) {
|
|
154
|
+
const missingProps = [];
|
|
155
|
+
for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
|
|
156
|
+
if (!(fieldKey in parsed)) {
|
|
157
|
+
missingProps.push(fieldKey);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`Missing required fields: ${missingProps.join(", ")}`);
|
|
75
161
|
}
|
|
76
|
-
|
|
162
|
+
postprocessParsedReceipt(parsed);
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
77
165
|
export {
|
|
78
|
-
|
|
166
|
+
parseReceipt
|
|
79
167
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamtamchik/app-store-receipt-parser",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A lightweight TypeScript library for extracting transaction IDs from Apple's ASN.1 encoded receipts.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -34,15 +34,15 @@
|
|
|
34
34
|
"asn1js": "3.0.5"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@types/jest": "29.5.
|
|
38
|
-
"@types/node": "20.
|
|
39
|
-
"@typescript-eslint/eslint-plugin": "6.
|
|
40
|
-
"@typescript-eslint/parser": "6.
|
|
41
|
-
"eslint": "8.
|
|
37
|
+
"@types/jest": "29.5.12",
|
|
38
|
+
"@types/node": "20.12.7",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "7.6.0",
|
|
40
|
+
"@typescript-eslint/parser": "7.6.0",
|
|
41
|
+
"eslint": "8.57.0",
|
|
42
42
|
"jest": "29.7.0",
|
|
43
|
-
"ts-jest": "29.1.
|
|
44
|
-
"tsup": "
|
|
45
|
-
"typescript": "5.
|
|
43
|
+
"ts-jest": "29.1.2",
|
|
44
|
+
"tsup": "8.0.2",
|
|
45
|
+
"typescript": "5.4.5"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsup src/index.ts --format cjs,esm --clean",
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Identifier for in-app receipt fields (starting with 17*) */
|
|
2
|
+
export const IN_APP: number = 17
|
|
3
|
+
|
|
4
|
+
/** Identifies pkcs7 content information encoded as Octet string */
|
|
5
|
+
export const CONTENT_ID = 'pkcs7_content'
|
|
6
|
+
|
|
7
|
+
/** Identifies field type id information */
|
|
8
|
+
export const FIELD_TYPE_ID: string = 'FieldType'
|
|
9
|
+
|
|
10
|
+
/** Identifies field value information encoded as Octet string */
|
|
11
|
+
export const FIELD_VALUE_ID: string = 'FieldTypeOctetString'
|
package/src/index.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export type { ParsedReceipt } from './parser'
|
|
2
|
+
export { parseReceipt } from './parser'
|
package/src/mappings.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type ReceiptFieldsKeyValues =
|
|
2
|
+
| 0
|
|
3
|
+
| 2
|
|
4
|
+
| 3
|
|
5
|
+
| 4
|
|
6
|
+
| 5
|
|
7
|
+
| 12
|
|
8
|
+
| 18
|
|
9
|
+
| 19
|
|
10
|
+
| 1701
|
|
11
|
+
| 1702
|
|
12
|
+
| 1703
|
|
13
|
+
| 1704
|
|
14
|
+
| 1705
|
|
15
|
+
| 1706
|
|
16
|
+
| 1708
|
|
17
|
+
| 1711
|
|
18
|
+
| 1712
|
|
19
|
+
|
|
20
|
+
export type ReceiptFieldsKeyNames =
|
|
21
|
+
| 'ENVIRONMENT'
|
|
22
|
+
| 'BUNDLE_ID'
|
|
23
|
+
| 'APP_VERSION'
|
|
24
|
+
| 'OPAQUE_VALUE'
|
|
25
|
+
| 'SHA1_HASH'
|
|
26
|
+
| 'RECEIPT_CREATION_DATE'
|
|
27
|
+
| 'ORIGINAL_PURCHASE_DATE'
|
|
28
|
+
| 'ORIGINAL_APP_VERSION'
|
|
29
|
+
| 'IN_APP_QUANTITY'
|
|
30
|
+
| 'IN_APP_PRODUCT_ID'
|
|
31
|
+
| 'IN_APP_TRANSACTION_ID'
|
|
32
|
+
| 'IN_APP_PURCHASE_DATE'
|
|
33
|
+
| 'IN_APP_ORIGINAL_TRANSACTION_ID'
|
|
34
|
+
| 'IN_APP_ORIGINAL_PURCHASE_DATE'
|
|
35
|
+
| 'IN_APP_EXPIRES_DATE'
|
|
36
|
+
| 'IN_APP_WEB_ORDER_LINE_ITEM_ID'
|
|
37
|
+
| 'IN_APP_CANCELLATION_DATE'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Receipt fields
|
|
41
|
+
* @see https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
|
|
42
|
+
*/
|
|
43
|
+
export const RECEIPT_FIELDS_MAP: ReadonlyMap<ReceiptFieldsKeyValues, ReceiptFieldsKeyNames> = new Map([
|
|
44
|
+
[0, 'ENVIRONMENT'],
|
|
45
|
+
[2, 'BUNDLE_ID'],
|
|
46
|
+
[3, 'APP_VERSION'],
|
|
47
|
+
[4, 'OPAQUE_VALUE'],
|
|
48
|
+
[5, 'SHA1_HASH'],
|
|
49
|
+
[12, 'RECEIPT_CREATION_DATE'],
|
|
50
|
+
[18, 'ORIGINAL_PURCHASE_DATE'],
|
|
51
|
+
[19, 'ORIGINAL_APP_VERSION'],
|
|
52
|
+
[1701, 'IN_APP_QUANTITY'],
|
|
53
|
+
[1702, 'IN_APP_PRODUCT_ID'],
|
|
54
|
+
[1703, 'IN_APP_TRANSACTION_ID'],
|
|
55
|
+
[1704, 'IN_APP_PURCHASE_DATE'],
|
|
56
|
+
[1705, 'IN_APP_ORIGINAL_TRANSACTION_ID'],
|
|
57
|
+
[1706, 'IN_APP_ORIGINAL_PURCHASE_DATE'],
|
|
58
|
+
[1708, 'IN_APP_EXPIRES_DATE'],
|
|
59
|
+
[1711, 'IN_APP_WEB_ORDER_LINE_ITEM_ID'],
|
|
60
|
+
[1712, 'IN_APP_CANCELLATION_DATE'],
|
|
61
|
+
])
|
|
62
|
+
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { IA5String, Integer, OctetString, Sequence, Set, Utf8String } from 'asn1js'
|
|
2
|
+
|
|
3
|
+
import { RECEIPT_FIELDS_MAP, ReceiptFieldsKeyNames, ReceiptFieldsKeyValues } from './mappings'
|
|
4
|
+
import { CONTENT_ID, FIELD_TYPE_ID, FIELD_VALUE_ID, IN_APP } from './constants'
|
|
5
|
+
import { verifyFieldSchema, verifyReceiptSchema } from './verifications'
|
|
6
|
+
import { uniqueArrayValues } from './utils'
|
|
7
|
+
|
|
8
|
+
export type Environment = 'Production' | 'ProductionSandbox' | string
|
|
9
|
+
|
|
10
|
+
export type ParsedReceipt = Partial<Record<ReceiptFieldsKeyNames, string>> & {
|
|
11
|
+
ENVIRONMENT: Environment
|
|
12
|
+
IN_APP_ORIGINAL_TRANSACTION_IDS: string[]
|
|
13
|
+
IN_APP_TRANSACTION_IDS: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isReceiptFieldKey (value: unknown): value is ReceiptFieldsKeyValues {
|
|
17
|
+
return Boolean(typeof value === 'number' && RECEIPT_FIELDS_MAP.has(value as ReceiptFieldsKeyValues))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isParsedReceiptContentComplete (data: ParsedReceipt): data is ParsedReceipt {
|
|
21
|
+
for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
|
|
22
|
+
if (!(fieldKey in data)) {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractFieldValue (field: OctetString): string {
|
|
31
|
+
const [fieldValue] = field.valueBlock.value
|
|
32
|
+
|
|
33
|
+
if (fieldValue instanceof IA5String || fieldValue instanceof Utf8String) {
|
|
34
|
+
return fieldValue.valueBlock.value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return field.toJSON().valueBlock.valueHex
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function appendField (parsed: ParsedReceipt, name: ReceiptFieldsKeyNames, value: string) {
|
|
41
|
+
if (name === 'IN_APP_ORIGINAL_TRANSACTION_ID') {
|
|
42
|
+
parsed.IN_APP_ORIGINAL_TRANSACTION_IDS.push(value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (name === 'IN_APP_TRANSACTION_ID') {
|
|
46
|
+
parsed.IN_APP_TRANSACTION_IDS.push(value)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
parsed[name] = value
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function processField (parsed: ParsedReceipt, fieldKey: number, fieldValue: OctetString) {
|
|
53
|
+
if (fieldKey === IN_APP) {
|
|
54
|
+
parseOctetStringContent(parsed, fieldValue)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!isReceiptFieldKey(fieldKey)) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const name = RECEIPT_FIELDS_MAP.get(fieldKey)!
|
|
63
|
+
appendField(parsed, name, extractFieldValue(fieldValue))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseOctetStringContent (parsed: ParsedReceipt, content: OctetString) {
|
|
67
|
+
const [contentSet] = content.valueBlock.value as Set[]
|
|
68
|
+
const contentSetSequences = contentSet.valueBlock.value.filter(v => v instanceof Sequence) as Sequence[]
|
|
69
|
+
|
|
70
|
+
for (const sequence of contentSetSequences) {
|
|
71
|
+
const verifiedSequence = verifyFieldSchema(sequence)
|
|
72
|
+
if (verifiedSequence) {
|
|
73
|
+
// We are confident to use "as" assertion because Integer type is guaranteed by positive verification above
|
|
74
|
+
const fieldKey = (verifiedSequence.result[FIELD_TYPE_ID] as Integer).valueBlock.valueDec
|
|
75
|
+
const fieldValueOctetString = verifiedSequence.result[FIELD_VALUE_ID] as OctetString
|
|
76
|
+
|
|
77
|
+
processField(parsed, fieldKey, fieldValueOctetString)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function postprocessParsedReceipt (parsed: ParsedReceipt) {
|
|
83
|
+
parsed.IN_APP_ORIGINAL_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_ORIGINAL_TRANSACTION_IDS)
|
|
84
|
+
parsed.IN_APP_TRANSACTION_IDS = uniqueArrayValues(parsed.IN_APP_TRANSACTION_IDS)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parseReceipt (receipt: string): ParsedReceipt {
|
|
88
|
+
const rootSchemaVerification = verifyReceiptSchema(receipt)
|
|
89
|
+
|
|
90
|
+
const content = rootSchemaVerification.result[CONTENT_ID] as OctetString
|
|
91
|
+
const parsed: ParsedReceipt = {
|
|
92
|
+
ENVIRONMENT: 'Production',
|
|
93
|
+
IN_APP_ORIGINAL_TRANSACTION_IDS: [],
|
|
94
|
+
IN_APP_TRANSACTION_IDS: [],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
parseOctetStringContent(parsed, content)
|
|
98
|
+
|
|
99
|
+
// Verify if the parsed content contains all the required fields
|
|
100
|
+
if (!isParsedReceiptContentComplete(parsed)) {
|
|
101
|
+
const missingProps = []
|
|
102
|
+
for (const fieldKey of RECEIPT_FIELDS_MAP.values()) {
|
|
103
|
+
if (!(fieldKey in parsed)) {
|
|
104
|
+
missingProps.push(fieldKey)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new Error(`Missing required fields: ${missingProps.join(', ')}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
postprocessParsedReceipt(parsed)
|
|
112
|
+
|
|
113
|
+
return parsed as ParsedReceipt
|
|
114
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const uniqueArrayValues = (array: string[]) => Array.from(new Set(array))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Any, Constructed, Integer, ObjectIdentifier, OctetString, Sequence, Set, verifySchema } from 'asn1js'
|
|
2
|
+
|
|
3
|
+
import { CONTENT_ID, FIELD_TYPE_ID, FIELD_VALUE_ID } from './constants'
|
|
4
|
+
|
|
5
|
+
const receiptSchema = new Sequence({
|
|
6
|
+
value: [
|
|
7
|
+
new ObjectIdentifier(),
|
|
8
|
+
new Constructed({
|
|
9
|
+
idBlock: { tagClass: 3, tagNumber: 0 },
|
|
10
|
+
value: [
|
|
11
|
+
new Sequence({
|
|
12
|
+
value: [
|
|
13
|
+
new Integer(),
|
|
14
|
+
new Set({
|
|
15
|
+
value: [
|
|
16
|
+
new Sequence({
|
|
17
|
+
value: [new ObjectIdentifier(), new Any()],
|
|
18
|
+
}),
|
|
19
|
+
],
|
|
20
|
+
}),
|
|
21
|
+
new Sequence({
|
|
22
|
+
value: [
|
|
23
|
+
new ObjectIdentifier(),
|
|
24
|
+
new Constructed({
|
|
25
|
+
idBlock: { tagClass: 3, tagNumber: 0 },
|
|
26
|
+
value: [new OctetString({ name: CONTENT_ID })],
|
|
27
|
+
}),
|
|
28
|
+
],
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
}),
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const fieldSchema = new Sequence({
|
|
38
|
+
value: [
|
|
39
|
+
new Integer({ name: FIELD_TYPE_ID }),
|
|
40
|
+
new Integer(),
|
|
41
|
+
new OctetString({ name: FIELD_VALUE_ID }),
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export function verifyReceiptSchema (receipt: string) {
|
|
46
|
+
const receiptVerification = verifySchema(Buffer.from(receipt, 'base64'), receiptSchema)
|
|
47
|
+
if (!receiptVerification.verified) {
|
|
48
|
+
throw new Error('Receipt verification failed.')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return receiptVerification
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function verifyFieldSchema (sequence: Sequence) {
|
|
55
|
+
const fieldVerification = verifySchema(sequence.toBER(), fieldSchema)
|
|
56
|
+
if (!fieldVerification.verified) {
|
|
57
|
+
// Return null if the field schema verification fails, so we can skip the field
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return fieldVerification
|
|
62
|
+
}
|
package/src/ReceiptParser.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { fromBER } from 'asn1js'
|
|
2
|
-
|
|
3
|
-
interface Block {
|
|
4
|
-
blockName: string
|
|
5
|
-
valueBlock: {
|
|
6
|
-
valueHex?: string
|
|
7
|
-
value?: Block[] | string
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface TransactionIds {
|
|
12
|
-
transactionIds: string[]
|
|
13
|
-
originalTransactionIds: string[]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Parses a receipt string and extracts the transaction IDs from it.
|
|
18
|
-
* Warning! This class does not validate the receipt string, only extracts them.
|
|
19
|
-
*/
|
|
20
|
-
export class ReceiptParser {
|
|
21
|
-
constructor (private readonly receiptString: string) {}
|
|
22
|
-
|
|
23
|
-
static getTransactionIdsFromBlock (block: Block): TransactionIds | null {
|
|
24
|
-
const { valueBlock } = block
|
|
25
|
-
|
|
26
|
-
if (!valueBlock || !Array.isArray(valueBlock.value)) {
|
|
27
|
-
return null
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (valueBlock.value.length === 3) {
|
|
31
|
-
const result = this.extractTransactionIds(valueBlock.value)
|
|
32
|
-
if (result) return result
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const result: TransactionIds = { transactionIds: [], originalTransactionIds: [] }
|
|
36
|
-
|
|
37
|
-
for (const innerBlock of valueBlock.value) {
|
|
38
|
-
const innerIds = this.getTransactionIdsFromBlock(innerBlock)
|
|
39
|
-
if (innerIds) {
|
|
40
|
-
result.transactionIds.push(...innerIds.transactionIds)
|
|
41
|
-
result.originalTransactionIds.push(...innerIds.originalTransactionIds)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Deduplicate the IDs
|
|
46
|
-
result.transactionIds = [...new Set(result.transactionIds)]
|
|
47
|
-
result.originalTransactionIds = [...new Set(result.originalTransactionIds)]
|
|
48
|
-
|
|
49
|
-
return result
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
static getTransactionIdsFromReceiptString (receiptString: string): TransactionIds | null {
|
|
53
|
-
const { result } = fromBER(Buffer.from(receiptString, 'base64'))
|
|
54
|
-
|
|
55
|
-
if (result.error) {
|
|
56
|
-
throw new Error(`Error parsing receipt: ${result.error}`)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return this.getTransactionIdsFromBlock(result.toJSON() as Block)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* The transaction ID is encoded as an INTEGER with the value 0x06a7
|
|
64
|
-
*/
|
|
65
|
-
private static isTransactionIdBlock (block: Block): boolean {
|
|
66
|
-
return block.blockName === 'INTEGER' && block.valueBlock.valueHex === '06a7'
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The original transaction ID is encoded as an INTEGER with the value 0x06a9
|
|
71
|
-
*/
|
|
72
|
-
private static isOriginalTransactionIdBlock (block: Block): boolean {
|
|
73
|
-
return block.blockName === 'INTEGER' && block.valueBlock.valueHex === '06a9'
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private static extractIdFromBlock (block: Block): string | null {
|
|
77
|
-
if (!Array.isArray(block.valueBlock.value)) {
|
|
78
|
-
return null
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// The transaction ID is encoded as an OCTET STRING containing a UTF8String
|
|
82
|
-
if (block.blockName === 'OCTET STRING' && block.valueBlock.value[0].blockName === 'UTF8String') {
|
|
83
|
-
return block.valueBlock.value[0].valueBlock.value as string
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return null
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private static extractTransactionIds (blocks: Block[]): TransactionIds | null {
|
|
90
|
-
const [firstBlock, , lastBlock] = blocks
|
|
91
|
-
const result: TransactionIds = { transactionIds: [], originalTransactionIds: [] }
|
|
92
|
-
|
|
93
|
-
if (this.isTransactionIdBlock(firstBlock)) {
|
|
94
|
-
const id = this.extractIdFromBlock(lastBlock)
|
|
95
|
-
if (id) result.transactionIds.push(id)
|
|
96
|
-
} else if (this.isOriginalTransactionIdBlock(firstBlock)) {
|
|
97
|
-
const id = this.extractIdFromBlock(lastBlock)
|
|
98
|
-
if (id) result.originalTransactionIds.push(id)
|
|
99
|
-
} else {
|
|
100
|
-
return null
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return result
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
getTransactionIds (): TransactionIds | null {
|
|
107
|
-
return ReceiptParser.getTransactionIdsFromReceiptString(this.receiptString)
|
|
108
|
-
}
|
|
109
|
-
}
|