@parity/product-deploy 0.7.28-rc.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/LICENSE +201 -0
- package/README.md +233 -0
- package/assets/environments.json +313 -0
- package/bin/bulletin-bootstrap +84 -0
- package/bin/bulletin-deploy +429 -0
- package/dist/bug-report.d.ts +29 -0
- package/dist/bug-report.js +27 -0
- package/dist/chunk-2VAUMZB2.js +284 -0
- package/dist/chunk-43HLT335.js +232 -0
- package/dist/chunk-5VZQ2KSU.js +231 -0
- package/dist/chunk-ADNBLFDP.js +225 -0
- package/dist/chunk-BMAEWZYV.js +24 -0
- package/dist/chunk-C2TS5MER.js +64 -0
- package/dist/chunk-DNXH4QTI.js +2336 -0
- package/dist/chunk-FZWJV5AD.js +231 -0
- package/dist/chunk-GZD2UFLR.js +8 -0
- package/dist/chunk-HOTQDYHD.js +219 -0
- package/dist/chunk-IDYGYIMH.js +207 -0
- package/dist/chunk-KHVTYIIX.js +146 -0
- package/dist/chunk-KJH2T5TQ.js +172 -0
- package/dist/chunk-KOSF5FDO.js +49 -0
- package/dist/chunk-LZJMVPYW.js +156 -0
- package/dist/chunk-MFTODIIT.js +725 -0
- package/dist/chunk-MMAZFJDG.js +91 -0
- package/dist/chunk-NF2FL4ZO.js +164 -0
- package/dist/chunk-OITUIM2E.js +524 -0
- package/dist/chunk-P6CHOMN3.js +2368 -0
- package/dist/chunk-QMYW3D6E.js +316 -0
- package/dist/chunk-QTZNULSH.js +185 -0
- package/dist/chunk-RI3ZLNPN.js +71 -0
- package/dist/chunk-S7EM5VMW.js +108 -0
- package/dist/chunk-T7EEVWNU.js +32 -0
- package/dist/chunk-UPWEOGLQ.js +37 -0
- package/dist/chunk-ZOC4GITL.js +13 -0
- package/dist/chunk-ZYVGHDMU.js +117 -0
- package/dist/chunk-probe.d.ts +37 -0
- package/dist/chunk-probe.js +18 -0
- package/dist/chunker.d.ts +8 -0
- package/dist/chunker.js +10 -0
- package/dist/deploy.d.ts +299 -0
- package/dist/deploy.js +96 -0
- package/dist/dotns.d.ts +506 -0
- package/dist/dotns.js +101 -0
- package/dist/environments.d.ts +104 -0
- package/dist/environments.js +23 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +8 -0
- package/dist/gh-pages-mirror.d.ts +76 -0
- package/dist/gh-pages-mirror.js +30 -0
- package/dist/incremental-stats.d.ts +69 -0
- package/dist/incremental-stats.js +10 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +146 -0
- package/dist/manifest/byte-budget.d.ts +46 -0
- package/dist/manifest/byte-budget.js +14 -0
- package/dist/manifest/config-load.d.ts +36 -0
- package/dist/manifest/config-load.js +10 -0
- package/dist/manifest/publish.d.ts +54 -0
- package/dist/manifest/publish.js +23 -0
- package/dist/manifest/schema.d.ts +29 -0
- package/dist/manifest/schema.js +10 -0
- package/dist/manifest/types.d.ts +90 -0
- package/dist/manifest/types.js +6 -0
- package/dist/manifest-embed.d.ts +18 -0
- package/dist/manifest-embed.js +9 -0
- package/dist/manifest-fetch.d.ts +32 -0
- package/dist/manifest-fetch.js +21 -0
- package/dist/manifest-roundtrip.d.ts +15 -0
- package/dist/manifest-roundtrip.js +55 -0
- package/dist/manifest.d.ts +44 -0
- package/dist/manifest.js +20 -0
- package/dist/memory-report.d.ts +95 -0
- package/dist/memory-report.js +17 -0
- package/dist/merkle.d.ts +50 -0
- package/dist/merkle.js +33 -0
- package/dist/personhood/bind-paid-alias.d.ts +43 -0
- package/dist/personhood/bind-paid-alias.js +10 -0
- package/dist/personhood/bind-personal-id.d.ts +55 -0
- package/dist/personhood/bind-personal-id.js +12 -0
- package/dist/personhood/bootstrap.d.ts +85 -0
- package/dist/personhood/bootstrap.js +245 -0
- package/dist/personhood/claim-pgas.d.ts +61 -0
- package/dist/personhood/claim-pgas.js +12 -0
- package/dist/personhood/constants.d.ts +23 -0
- package/dist/personhood/constants.js +22 -0
- package/dist/personhood/encoding.d.ts +49 -0
- package/dist/personhood/encoding.js +24 -0
- package/dist/personhood/hashing.d.ts +4 -0
- package/dist/personhood/hashing.js +8 -0
- package/dist/personhood/member-key.d.ts +12 -0
- package/dist/personhood/member-key.js +10 -0
- package/dist/personhood/people-client.d.ts +14 -0
- package/dist/personhood/people-client.js +48 -0
- package/dist/personhood/reprove.d.ts +43 -0
- package/dist/personhood/reprove.js +225 -0
- package/dist/pool.d.ts +51 -0
- package/dist/pool.js +30 -0
- package/dist/run-state.d.ts +22 -0
- package/dist/run-state.js +20 -0
- package/dist/telemetry.d.ts +56 -0
- package/dist/telemetry.js +71 -0
- package/dist/version-check.d.ts +38 -0
- package/dist/version-check.js +30 -0
- package/docs/bootstrap.md +49 -0
- package/docs/e2e-bootstrap.md +154 -0
- package/docs/telemetry.md +62 -0
- package/docs/testing.md +44 -0
- package/package.json +82 -0
- package/tools/release-retry-wrapper.mjs +74 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// src/pool.ts
|
|
2
|
+
import { sr25519CreateDerive } from "@polkadot-labs/hdkd";
|
|
3
|
+
import { DEV_PHRASE, entropyToMiniSecret, mnemonicToEntropy } from "@polkadot-labs/hdkd-helpers";
|
|
4
|
+
import { createClient, Enum } from "polkadot-api";
|
|
5
|
+
import { getPolkadotSigner } from "polkadot-api/signer";
|
|
6
|
+
import { getWsProvider } from "polkadot-api/ws";
|
|
7
|
+
import { Keyring } from "@polkadot/keyring";
|
|
8
|
+
import { cryptoWaitReady } from "@polkadot/util-crypto";
|
|
9
|
+
var PAS_DECIMALS_DIVISOR = 1e10;
|
|
10
|
+
function formatPasBalance(plancks) {
|
|
11
|
+
return (Number(plancks) / PAS_DECIMALS_DIVISOR).toFixed(4);
|
|
12
|
+
}
|
|
13
|
+
var DEPLOY_PATH_PREFIX = "//deploy";
|
|
14
|
+
var TOPUP_TRANSACTIONS = 1e3;
|
|
15
|
+
var TOPUP_BYTES = 100000000n;
|
|
16
|
+
var WS_HEARTBEAT_TIMEOUT_MS = 3e5;
|
|
17
|
+
var AUTHORIZATION_EXTENSION_BLOCKS = 2e6;
|
|
18
|
+
function derivePoolAccounts(poolSize = 10, mnemonic = DEV_PHRASE) {
|
|
19
|
+
const entropy = mnemonicToEntropy(mnemonic);
|
|
20
|
+
const miniSecret = entropyToMiniSecret(entropy);
|
|
21
|
+
const derive = sr25519CreateDerive(miniSecret);
|
|
22
|
+
const keyring = new Keyring({ type: "sr25519" });
|
|
23
|
+
const accounts = [];
|
|
24
|
+
for (let i = 0; i < poolSize; i++) {
|
|
25
|
+
const path = `${DEPLOY_PATH_PREFIX}/${i}`;
|
|
26
|
+
const keyPair = derive(path);
|
|
27
|
+
const signer = getPolkadotSigner(keyPair.publicKey, "Sr25519", keyPair.sign);
|
|
28
|
+
const address = keyring.encodeAddress(keyPair.publicKey);
|
|
29
|
+
accounts.push({ index: i, path, publicKey: keyPair.publicKey, signer, address });
|
|
30
|
+
}
|
|
31
|
+
return accounts;
|
|
32
|
+
}
|
|
33
|
+
function isAuthorizationSufficient(auth, currentBlock, check = {}) {
|
|
34
|
+
if (auth === void 0) return false;
|
|
35
|
+
if (Number(auth.expiration ?? 0) <= currentBlock) return false;
|
|
36
|
+
if (check.bulletinAuthorizeV2 && check.needs) {
|
|
37
|
+
const txsRemaining = BigInt(auth.extent.transactions_allowance) - BigInt(auth.extent.transactions);
|
|
38
|
+
const bytesRemaining = BigInt(auth.extent.bytes_allowance) - BigInt(auth.extent.bytes);
|
|
39
|
+
if (txsRemaining < check.needs.txs) return false;
|
|
40
|
+
if (bytesRemaining < check.needs.bytes) return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
function selectAccount(authorizations, random = Math.random, currentBlock) {
|
|
45
|
+
const eligible = authorizations.filter(
|
|
46
|
+
(a) => currentBlock === void 0 || a.expiration > currentBlock
|
|
47
|
+
);
|
|
48
|
+
if (eligible.length === 0) return null;
|
|
49
|
+
return { account: eligible[Math.floor(random() * eligible.length)], eligibleCount: eligible.length };
|
|
50
|
+
}
|
|
51
|
+
async function fetchPoolAuthorizations(api, accounts) {
|
|
52
|
+
const results = await Promise.all(
|
|
53
|
+
accounts.map(async (account) => {
|
|
54
|
+
try {
|
|
55
|
+
const auth = await api.query.TransactionStorage.Authorizations.getValue(
|
|
56
|
+
Enum("Account", account.address)
|
|
57
|
+
);
|
|
58
|
+
return {
|
|
59
|
+
...account,
|
|
60
|
+
transactions: auth ? BigInt(auth.extent.transactions_allowance) - BigInt(auth.extent.transactions) : 0n,
|
|
61
|
+
bytes: auth ? BigInt(auth.extent.bytes_allowance) - BigInt(auth.extent.bytes) : 0n,
|
|
62
|
+
expiration: auth ? Number(auth.expiration) : 0
|
|
63
|
+
};
|
|
64
|
+
} catch {
|
|
65
|
+
return { ...account, transactions: 0n, bytes: 0n, expiration: 0 };
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
function computeTopUpTarget(current, needs) {
|
|
72
|
+
const minTxs = needs.txs + BigInt(TOPUP_TRANSACTIONS);
|
|
73
|
+
const minBytes = needs.bytes + TOPUP_BYTES;
|
|
74
|
+
if (current.transactions >= minTxs && current.bytes >= minBytes) return null;
|
|
75
|
+
return {
|
|
76
|
+
transactions: current.transactions > minTxs ? current.transactions : minTxs,
|
|
77
|
+
bytes: current.bytes > minBytes ? current.bytes : minBytes
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function classifyAliceTxError(err) {
|
|
81
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
82
|
+
const lower = msg.toLowerCase();
|
|
83
|
+
if (/\bstale\b/.test(lower)) return "retry";
|
|
84
|
+
if (/"type"\s*:\s*"future"|\binvalid::future\b/.test(lower)) return "retry";
|
|
85
|
+
if (lower.includes("websocket") || lower.includes("connection") || lower.includes("socket closed") || lower.includes("disconnect")) return "retry";
|
|
86
|
+
if (lower.includes("timed out") || lower.includes("timeout")) return "retry";
|
|
87
|
+
return "abort";
|
|
88
|
+
}
|
|
89
|
+
var ALICE_MAX_ATTEMPTS = 3;
|
|
90
|
+
async function submitAliceTxWithRetry(buildTx, aliceSigner, label) {
|
|
91
|
+
let lastError;
|
|
92
|
+
let attempts = 0;
|
|
93
|
+
for (let attempt = 1; attempt <= ALICE_MAX_ATTEMPTS; attempt++) {
|
|
94
|
+
attempts = attempt;
|
|
95
|
+
try {
|
|
96
|
+
const tx = buildTx();
|
|
97
|
+
const result = await tx.signAndSubmit(aliceSigner);
|
|
98
|
+
if (!result.ok) throw new Error(`${label} dispatch error`);
|
|
99
|
+
return;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
lastError = e;
|
|
102
|
+
const decision = classifyAliceTxError(e);
|
|
103
|
+
if (decision === "abort" || attempt === ALICE_MAX_ATTEMPTS) break;
|
|
104
|
+
console.log(` ${label}: attempt ${attempt}/${ALICE_MAX_ATTEMPTS} failed (${String(e.message ?? e).slice(0, 80)}), retrying...`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
108
|
+
const plural = attempts === 1 ? "attempt" : "attempts";
|
|
109
|
+
throw new Error(`${label} failed after ${attempts} ${plural}: ${msg}`);
|
|
110
|
+
}
|
|
111
|
+
function isTestnetSpecName(specName) {
|
|
112
|
+
if (!specName) return false;
|
|
113
|
+
const s = specName.toLowerCase();
|
|
114
|
+
if (s.includes("paseo")) return true;
|
|
115
|
+
if (/\b(westend|rococo)\b/.test(s)) return true;
|
|
116
|
+
if (/\b(testnet|devnet)\b/.test(s)) return true;
|
|
117
|
+
if (/-test$|-testnet$|-dev$/.test(s)) return true;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
var _testnetDetectionCache = null;
|
|
121
|
+
async function detectTestnet(api) {
|
|
122
|
+
if (_testnetDetectionCache !== null) return _testnetDetectionCache;
|
|
123
|
+
try {
|
|
124
|
+
const version = await api.constants.System.Version();
|
|
125
|
+
const raw = version?.spec_name ?? version?.specName;
|
|
126
|
+
const specName = typeof raw === "string" ? raw : raw?.asText?.() ?? String(raw ?? "");
|
|
127
|
+
_testnetDetectionCache = isTestnetSpecName(specName);
|
|
128
|
+
} catch {
|
|
129
|
+
_testnetDetectionCache = false;
|
|
130
|
+
}
|
|
131
|
+
return _testnetDetectionCache;
|
|
132
|
+
}
|
|
133
|
+
function _resetTestnetCacheForTests() {
|
|
134
|
+
_testnetDetectionCache = null;
|
|
135
|
+
}
|
|
136
|
+
function aliceKeyring() {
|
|
137
|
+
const keyring = new Keyring({ type: "sr25519" });
|
|
138
|
+
const alice = keyring.addFromUri("//Alice");
|
|
139
|
+
const signer = getPolkadotSigner(alice.publicKey, "Sr25519", (data) => alice.sign(data));
|
|
140
|
+
return { alice, signer };
|
|
141
|
+
}
|
|
142
|
+
var U32_MAX = 0xFFFFFFFFn;
|
|
143
|
+
function clampU32(n, name) {
|
|
144
|
+
if (n < 0n) throw new Error(`${name} must be non-negative`);
|
|
145
|
+
if (n > U32_MAX) throw new Error(`${name} (${n}) exceeds u32 max \u2014 split the deploy into smaller batches`);
|
|
146
|
+
return Number(n);
|
|
147
|
+
}
|
|
148
|
+
async function ensureAuthorized(api, address, label, bulletinAuthorizeV2, needs) {
|
|
149
|
+
const [auth, currentBlock] = await Promise.all([
|
|
150
|
+
api.query.TransactionStorage.Authorizations.getValue(Enum("Account", address)),
|
|
151
|
+
api.query.System.Number.getValue()
|
|
152
|
+
]);
|
|
153
|
+
if (isAuthorizationSufficient(auth, currentBlock, { needs, bulletinAuthorizeV2 })) return;
|
|
154
|
+
console.log(` Auto-authorizing ${label ?? "account"} (${address.slice(0, 8)}...)...`);
|
|
155
|
+
const { signer } = aliceKeyring();
|
|
156
|
+
if (bulletinAuthorizeV2) {
|
|
157
|
+
await submitAliceTxWithRetry(
|
|
158
|
+
() => api.tx.TransactionStorage.authorize_account({
|
|
159
|
+
who: address,
|
|
160
|
+
transactions: clampU32(BigInt(TOPUP_TRANSACTIONS), "transactions"),
|
|
161
|
+
bytes: TOPUP_BYTES
|
|
162
|
+
}),
|
|
163
|
+
signer,
|
|
164
|
+
`authorize_account(${label ?? "account"})`
|
|
165
|
+
);
|
|
166
|
+
console.log(` Authorized: ${TOPUP_TRANSACTIONS} txs / ${Number(TOPUP_BYTES) / 1e6}MB`);
|
|
167
|
+
} else {
|
|
168
|
+
const newExpiration = currentBlock + AUTHORIZATION_EXTENSION_BLOCKS;
|
|
169
|
+
await submitAliceTxWithRetry(
|
|
170
|
+
() => api.tx.TransactionStorage.authorize_account({
|
|
171
|
+
who: address,
|
|
172
|
+
expiration: newExpiration
|
|
173
|
+
}),
|
|
174
|
+
signer,
|
|
175
|
+
`authorize_account(${label ?? "account"})`
|
|
176
|
+
);
|
|
177
|
+
console.log(` Authorized: expires at block ${newExpiration} (current: ${currentBlock})`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function topUpBy(api, address, needs, label, bulletinAuthorizeV2) {
|
|
181
|
+
const [currentAuth, currentBlock] = await Promise.all([
|
|
182
|
+
api.query.TransactionStorage.Authorizations.getValue(Enum("Account", address)),
|
|
183
|
+
api.query.System.Number.getValue()
|
|
184
|
+
]);
|
|
185
|
+
if (isAuthorizationSufficient(currentAuth, currentBlock, { needs, bulletinAuthorizeV2 })) {
|
|
186
|
+
const expiration = Number(currentAuth.expiration);
|
|
187
|
+
if (currentAuth.extent) {
|
|
188
|
+
const fmtMB = (b) => (Number(b) / 1e6).toFixed(1);
|
|
189
|
+
const txsRemaining = BigInt(currentAuth.extent.transactions_allowance) - BigInt(currentAuth.extent.transactions);
|
|
190
|
+
const bytesRemaining = BigInt(currentAuth.extent.bytes_allowance) - BigInt(currentAuth.extent.bytes);
|
|
191
|
+
console.log(` Pre-auth skipped for ${label ?? "account"} (${address.slice(0, 8)}...): authorized until block ${expiration}, ${txsRemaining} txs / ${fmtMB(bytesRemaining)}MB remaining.`);
|
|
192
|
+
} else {
|
|
193
|
+
console.log(` Pre-auth skipped for ${label ?? "account"} (${address.slice(0, 8)}...): authorized until block ${expiration}.`);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const { signer } = aliceKeyring();
|
|
198
|
+
if (bulletinAuthorizeV2) {
|
|
199
|
+
console.log(` Pre-authorizing ${label ?? "account"} (${address.slice(0, 8)}...): granting ${TOPUP_TRANSACTIONS} txs / ${Number(TOPUP_BYTES) / 1e6}MB...`);
|
|
200
|
+
await submitAliceTxWithRetry(
|
|
201
|
+
() => api.tx.TransactionStorage.authorize_account({
|
|
202
|
+
who: address,
|
|
203
|
+
transactions: clampU32(BigInt(TOPUP_TRANSACTIONS), "transactions"),
|
|
204
|
+
bytes: TOPUP_BYTES
|
|
205
|
+
}),
|
|
206
|
+
signer,
|
|
207
|
+
`topUpBy(${label ?? "account"})`
|
|
208
|
+
);
|
|
209
|
+
console.log(` Pre-authorized: ${TOPUP_TRANSACTIONS} txs / ${Number(TOPUP_BYTES) / 1e6}MB`);
|
|
210
|
+
} else {
|
|
211
|
+
const newExpiration = currentBlock + AUTHORIZATION_EXTENSION_BLOCKS;
|
|
212
|
+
console.log(` Pre-authorizing ${label ?? "account"} (${address.slice(0, 8)}...): extending authorization to block ${newExpiration}...`);
|
|
213
|
+
await submitAliceTxWithRetry(
|
|
214
|
+
() => api.tx.TransactionStorage.authorize_account({
|
|
215
|
+
who: address,
|
|
216
|
+
expiration: newExpiration
|
|
217
|
+
}),
|
|
218
|
+
signer,
|
|
219
|
+
`topUpBy(${label ?? "account"})`
|
|
220
|
+
);
|
|
221
|
+
console.log(` Pre-authorized: expires at block ${newExpiration}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function bootstrapPool(bulletinRpc, poolSize = 10, mnemonic, opts = {}) {
|
|
225
|
+
console.log(`Bootstrapping ${poolSize} pool accounts on ${bulletinRpc}...
|
|
226
|
+
`);
|
|
227
|
+
await cryptoWaitReady();
|
|
228
|
+
const accounts = derivePoolAccounts(poolSize, mnemonic);
|
|
229
|
+
const client = createClient(getWsProvider(
|
|
230
|
+
bulletinRpc,
|
|
231
|
+
{ heartbeatTimeout: WS_HEARTBEAT_TIMEOUT_MS }
|
|
232
|
+
));
|
|
233
|
+
const api = client.getUnsafeApi();
|
|
234
|
+
const keyring = new Keyring({ type: "sr25519" });
|
|
235
|
+
const alice = keyring.addFromUri("//Alice");
|
|
236
|
+
const aliceSigner = getPolkadotSigner(alice.publicKey, "Sr25519", (data) => alice.sign(data));
|
|
237
|
+
const aliceAccount = await api.query.System.Account.getValue(alice.address);
|
|
238
|
+
const aliceBalance = BigInt(aliceAccount.data.free);
|
|
239
|
+
console.log(`Alice balance: ${formatPasBalance(aliceBalance)} PAS
|
|
240
|
+
`);
|
|
241
|
+
const currentBlock = await api.query.System.Number.getValue();
|
|
242
|
+
if (opts.bulletinAuthorizeV2) {
|
|
243
|
+
console.log(`Authorizing accounts with V2 (${TOPUP_TRANSACTIONS} txs / ${Number(TOPUP_BYTES) / 1e6}MB)
|
|
244
|
+
`);
|
|
245
|
+
} else {
|
|
246
|
+
const authExpiration = currentBlock + AUTHORIZATION_EXTENSION_BLOCKS;
|
|
247
|
+
console.log(`Authorizing accounts until block ${authExpiration} (current: ${currentBlock})
|
|
248
|
+
`);
|
|
249
|
+
}
|
|
250
|
+
for (const account of accounts) {
|
|
251
|
+
console.log(`Account ${account.index}: ${account.address}`);
|
|
252
|
+
try {
|
|
253
|
+
let tx;
|
|
254
|
+
if (opts.bulletinAuthorizeV2) {
|
|
255
|
+
tx = api.tx.TransactionStorage.authorize_account({
|
|
256
|
+
who: account.address,
|
|
257
|
+
transactions: clampU32(BigInt(TOPUP_TRANSACTIONS), "transactions"),
|
|
258
|
+
bytes: TOPUP_BYTES
|
|
259
|
+
});
|
|
260
|
+
} else {
|
|
261
|
+
const authExpiration = currentBlock + AUTHORIZATION_EXTENSION_BLOCKS;
|
|
262
|
+
tx = api.tx.TransactionStorage.authorize_account({
|
|
263
|
+
who: account.address,
|
|
264
|
+
expiration: authExpiration
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const result = await tx.signAndSubmit(aliceSigner);
|
|
268
|
+
if (!result.ok) throw new Error("dispatch failed");
|
|
269
|
+
if (opts.bulletinAuthorizeV2) {
|
|
270
|
+
console.log(` Authorized: ${TOPUP_TRANSACTIONS} txs / ${Number(TOPUP_BYTES) / 1e6}MB`);
|
|
271
|
+
} else {
|
|
272
|
+
const authExpiration = currentBlock + AUTHORIZATION_EXTENSION_BLOCKS;
|
|
273
|
+
console.log(` Authorized: expires at block ${authExpiration}`);
|
|
274
|
+
}
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.log(` Authorization failed: ${e.message?.slice(0, 80)}`);
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const transfer = api.tx.Balances.transfer_allow_death({
|
|
280
|
+
dest: Enum("Id", account.address),
|
|
281
|
+
value: 100000000000n
|
|
282
|
+
// 0.1 PAS
|
|
283
|
+
});
|
|
284
|
+
const result = await transfer.signAndSubmit(aliceSigner);
|
|
285
|
+
if (!result.ok) throw new Error("dispatch failed");
|
|
286
|
+
console.log(` Transferred 0.1 PAS`);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.log(` Transfer failed: ${e.message?.slice(0, 80)}`);
|
|
289
|
+
}
|
|
290
|
+
console.log("");
|
|
291
|
+
}
|
|
292
|
+
console.log("=".repeat(60));
|
|
293
|
+
console.log("Pool Summary");
|
|
294
|
+
console.log("=".repeat(60));
|
|
295
|
+
const auths = await fetchPoolAuthorizations(api, accounts);
|
|
296
|
+
for (const a of auths) {
|
|
297
|
+
console.log(` ${a.index}: ${a.address} | txs: ${a.transactions} | bytes: ${Number(a.bytes) / 1e6}MB`);
|
|
298
|
+
}
|
|
299
|
+
client.destroy();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export {
|
|
303
|
+
formatPasBalance,
|
|
304
|
+
derivePoolAccounts,
|
|
305
|
+
isAuthorizationSufficient,
|
|
306
|
+
selectAccount,
|
|
307
|
+
fetchPoolAuthorizations,
|
|
308
|
+
computeTopUpTarget,
|
|
309
|
+
classifyAliceTxError,
|
|
310
|
+
isTestnetSpecName,
|
|
311
|
+
detectTestnet,
|
|
312
|
+
_resetTestnetCacheForTests,
|
|
313
|
+
ensureAuthorized,
|
|
314
|
+
topUpBy,
|
|
315
|
+
bootstrapPool
|
|
316
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureWarning
|
|
3
|
+
} from "./chunk-MFTODIIT.js";
|
|
4
|
+
|
|
5
|
+
// src/chunk-probe.ts
|
|
6
|
+
import { Twox128, Blake2128Concat, decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings";
|
|
7
|
+
import { CID } from "multiformats/cid";
|
|
8
|
+
var ChainProbeMetadataError = class extends Error {
|
|
9
|
+
constructor(msg) {
|
|
10
|
+
super(msg);
|
|
11
|
+
this.name = "ChainProbeMetadataError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var ChainProbeCrossValidationError = class extends Error {
|
|
15
|
+
constructor(msg) {
|
|
16
|
+
super(msg);
|
|
17
|
+
this.name = "ChainProbeCrossValidationError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var _enc = new TextEncoder();
|
|
21
|
+
var MAX_BLOCK = 2 ** 30;
|
|
22
|
+
var MAX_TX_INDEX = 512;
|
|
23
|
+
var DEFAULT_BATCH_SIZE = 500;
|
|
24
|
+
var _metadataChecked = false;
|
|
25
|
+
var _crossValidated = false;
|
|
26
|
+
var _sentryBatchCount = 0;
|
|
27
|
+
function buildStorageKey(contentHashBytes) {
|
|
28
|
+
const parts = [
|
|
29
|
+
Twox128(_enc.encode("TransactionStorage")),
|
|
30
|
+
Twox128(_enc.encode("TransactionByContentHash")),
|
|
31
|
+
Blake2128Concat(contentHashBytes)
|
|
32
|
+
];
|
|
33
|
+
const total = parts.reduce((s, p) => s + p.length, 0);
|
|
34
|
+
const key = new Uint8Array(total);
|
|
35
|
+
let offset = 0;
|
|
36
|
+
for (const p of parts) {
|
|
37
|
+
key.set(p, offset);
|
|
38
|
+
offset += p.length;
|
|
39
|
+
}
|
|
40
|
+
return "0x" + Buffer.from(key).toString("hex");
|
|
41
|
+
}
|
|
42
|
+
function cidToContentHash(cidStr) {
|
|
43
|
+
return CID.parse(cidStr).multihash.digest;
|
|
44
|
+
}
|
|
45
|
+
function hexToBytes(hex) {
|
|
46
|
+
return Buffer.from(hex.replace(/^0x/, ""), "hex");
|
|
47
|
+
}
|
|
48
|
+
function _decodeStorageValue(hex) {
|
|
49
|
+
if (!hex || hex === "0x" || hex === "0x00") return null;
|
|
50
|
+
const bytes = hexToBytes(hex);
|
|
51
|
+
if (bytes.length < 8) return null;
|
|
52
|
+
const block = bytes.readUInt32LE(0);
|
|
53
|
+
const index = bytes.readUInt32LE(4);
|
|
54
|
+
if (block <= 0 || block >= MAX_BLOCK || index >= MAX_TX_INDEX) return null;
|
|
55
|
+
return { block, index };
|
|
56
|
+
}
|
|
57
|
+
async function ensureMetadataChecked(client) {
|
|
58
|
+
if (_metadataChecked) return;
|
|
59
|
+
const metaHex = await client._request("state_getMetadata", []);
|
|
60
|
+
const decoded = decAnyMetadata(hexToBytes(metaHex));
|
|
61
|
+
const unified = unifyMetadata(decoded);
|
|
62
|
+
const pallet = unified.pallets?.find((p) => p.name === "TransactionStorage");
|
|
63
|
+
if (!pallet) throw new ChainProbeMetadataError("TransactionStorage pallet not found in runtime metadata");
|
|
64
|
+
const item = pallet.storage?.items?.find((e) => e.name === "TransactionByContentHash");
|
|
65
|
+
if (!item) throw new ChainProbeMetadataError("TransactionByContentHash entry not found in TransactionStorage");
|
|
66
|
+
if (item.type?.tag !== "map") {
|
|
67
|
+
throw new ChainProbeMetadataError(
|
|
68
|
+
`TransactionByContentHash storage type is '${item.type?.tag}', expected 'map'`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const hasher = item.type.value?.hashers?.[0]?.tag;
|
|
72
|
+
if (hasher !== "Blake2128Concat") {
|
|
73
|
+
throw new ChainProbeMetadataError(
|
|
74
|
+
`TransactionByContentHash key hasher is '${hasher}', expected 'Blake2128Concat'. Update key construction in chunk-probe.ts.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
_metadataChecked = true;
|
|
78
|
+
}
|
|
79
|
+
async function crossValidateFirstHit(client, cid, block, index, contentHashBytes) {
|
|
80
|
+
if (_crossValidated) return;
|
|
81
|
+
_crossValidated = true;
|
|
82
|
+
try {
|
|
83
|
+
const blockBuf = Buffer.alloc(4);
|
|
84
|
+
blockBuf.writeUInt32LE(block, 0);
|
|
85
|
+
const txKey = "0x" + Buffer.from([
|
|
86
|
+
...Twox128(_enc.encode("TransactionStorage")),
|
|
87
|
+
...Twox128(_enc.encode("Transactions")),
|
|
88
|
+
...Blake2128Concat(blockBuf)
|
|
89
|
+
]).toString("hex");
|
|
90
|
+
const result = await client._request("state_queryStorageAt", [[txKey]]);
|
|
91
|
+
const hex = result[0]?.changes?.[0]?.[1];
|
|
92
|
+
if (!hex) {
|
|
93
|
+
captureWarning("chunk-probe cross-validate: Transactions[block] absent (non-fatal)", { cid, block, index });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const haystack = hexToBytes(hex);
|
|
97
|
+
const needle = Buffer.from(contentHashBytes);
|
|
98
|
+
if (!haystack.includes(needle)) {
|
|
99
|
+
throw new ChainProbeCrossValidationError(
|
|
100
|
+
`Cross-validation failed: content hash for CID ${cid} not found in Transactions[${block}]. Key construction may be wrong. Run tools/chain-probe-key-probe.mjs to diagnose.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
} catch (e) {
|
|
104
|
+
if (e instanceof ChainProbeCrossValidationError) throw e;
|
|
105
|
+
captureWarning("chunk-probe cross-validate RPC error (non-fatal)", { cid, error: String(e).slice(0, 200) });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function probeChunks(cids, options) {
|
|
109
|
+
if (cids.length === 0) return [];
|
|
110
|
+
const { client, batchSize = DEFAULT_BATCH_SIZE, atFinalized } = options;
|
|
111
|
+
try {
|
|
112
|
+
await ensureMetadataChecked(client);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (e instanceof ChainProbeMetadataError) throw e;
|
|
115
|
+
return cids.map((cid) => ({ cid, present: null, failureReason: "metadata_error" }));
|
|
116
|
+
}
|
|
117
|
+
let atHash;
|
|
118
|
+
if (atFinalized) {
|
|
119
|
+
try {
|
|
120
|
+
atHash = await client._request("chain_getFinalizedHead", []);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
captureWarning("chunk-probe: chain_getFinalizedHead failed, probing best-chain", { error: String(e).slice(0, 200) });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const results = new Array(cids.length);
|
|
126
|
+
for (let start = 0; start < cids.length; start += batchSize) {
|
|
127
|
+
const batchCids = cids.slice(start, start + batchSize);
|
|
128
|
+
const batchDigests = batchCids.map(cidToContentHash);
|
|
129
|
+
const batchKeys = batchDigests.map(buildStorageKey);
|
|
130
|
+
let changes;
|
|
131
|
+
try {
|
|
132
|
+
const rpcResult = await client._request("state_queryStorageAt", atHash ? [batchKeys, atHash] : [batchKeys]);
|
|
133
|
+
changes = rpcResult[0]?.changes ?? [];
|
|
134
|
+
} catch {
|
|
135
|
+
for (let i = 0; i < batchCids.length; i++) {
|
|
136
|
+
results[start + i] = { cid: batchCids[i], present: null, failureReason: "rpc_error" };
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const keyToValue = new Map(changes);
|
|
141
|
+
for (let i = 0; i < batchCids.length; i++) {
|
|
142
|
+
const cid = batchCids[i];
|
|
143
|
+
const key = batchKeys[i];
|
|
144
|
+
const rawValue = keyToValue.get(key) ?? null;
|
|
145
|
+
if (!rawValue) {
|
|
146
|
+
results[start + i] = { cid, present: false };
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const decoded = _decodeStorageValue(rawValue);
|
|
150
|
+
if (!decoded) {
|
|
151
|
+
if (_sentryBatchCount < 3) {
|
|
152
|
+
captureWarning("chunk-probe decode failed (out-of-range or short value)", {
|
|
153
|
+
cid,
|
|
154
|
+
raw_hex: rawValue.slice(0, 64)
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
results[start + i] = { cid, present: null, failureReason: "decode_error" };
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
results[start + i] = { cid, present: true, block: decoded.block, index: decoded.index };
|
|
161
|
+
if (!_crossValidated) {
|
|
162
|
+
await crossValidateFirstHit(client, cid, decoded.block, decoded.index, batchDigests[i]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (_sentryBatchCount < 3) _sentryBatchCount++;
|
|
166
|
+
}
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
function _resetProbeSession() {
|
|
170
|
+
_metadataChecked = false;
|
|
171
|
+
_crossValidated = false;
|
|
172
|
+
_sentryBatchCount = 0;
|
|
173
|
+
}
|
|
174
|
+
function _bypassMetadataCheckForTest() {
|
|
175
|
+
_metadataChecked = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export {
|
|
179
|
+
ChainProbeMetadataError,
|
|
180
|
+
ChainProbeCrossValidationError,
|
|
181
|
+
_decodeStorageValue,
|
|
182
|
+
probeChunks,
|
|
183
|
+
_resetProbeSession,
|
|
184
|
+
_bypassMetadataCheckForTest
|
|
185
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/manifest/byte-budget.ts
|
|
2
|
+
var DEFAULT_TEXT_RECORD_BUDGET_BYTES = 1024;
|
|
3
|
+
var PLACEHOLDER_CID = "bafkreigh2akiscaildc7zh3vznzaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
4
|
+
function getTextRecordBudgetBytes() {
|
|
5
|
+
const raw = process.env.BULLETIN_TEXT_BUDGET;
|
|
6
|
+
if (raw === void 0 || raw === "") return DEFAULT_TEXT_RECORD_BUDGET_BYTES;
|
|
7
|
+
const parsed = Number.parseInt(raw, 10);
|
|
8
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
9
|
+
return DEFAULT_TEXT_RECORD_BUDGET_BYTES;
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
function assertWithinBudget(key, serialized, budget) {
|
|
13
|
+
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
14
|
+
return { ok: bytes <= budget, key, bytes, budget };
|
|
15
|
+
}
|
|
16
|
+
function pessimisticSizePreflight(config, budget = getTextRecordBudgetBytes()) {
|
|
17
|
+
const checks = [];
|
|
18
|
+
const placeholderRoot = {
|
|
19
|
+
$v: 1,
|
|
20
|
+
displayName: config.displayName,
|
|
21
|
+
description: config.description,
|
|
22
|
+
icon: { cid: PLACEHOLDER_CID, format: config.icon.format }
|
|
23
|
+
};
|
|
24
|
+
checks.push(
|
|
25
|
+
assertWithinBudget(
|
|
26
|
+
`${config.domain}#manifest`,
|
|
27
|
+
JSON.stringify(placeholderRoot),
|
|
28
|
+
budget
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
for (const exec of config.executables) {
|
|
32
|
+
const placeholder = composePlaceholderExecutable(exec);
|
|
33
|
+
checks.push(
|
|
34
|
+
assertWithinBudget(
|
|
35
|
+
`${exec.kind}.${config.domain}#executable`,
|
|
36
|
+
JSON.stringify(placeholder),
|
|
37
|
+
budget
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return { ok: checks.every((c) => c.ok), budget, checks };
|
|
42
|
+
}
|
|
43
|
+
function composePlaceholderExecutable(exec) {
|
|
44
|
+
if (exec.kind === "app") {
|
|
45
|
+
return { $v: 1, kind: "app", appVersion: exec.appVersion };
|
|
46
|
+
}
|
|
47
|
+
if (exec.kind === "widget") {
|
|
48
|
+
return {
|
|
49
|
+
$v: 1,
|
|
50
|
+
kind: "widget",
|
|
51
|
+
appVersion: exec.appVersion,
|
|
52
|
+
dimensions: exec.dimensions,
|
|
53
|
+
...exec.description !== void 0 ? { description: exec.description } : {}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
$v: 1,
|
|
58
|
+
kind: "worker",
|
|
59
|
+
appVersion: exec.appVersion,
|
|
60
|
+
entrypoint: exec.entrypoint,
|
|
61
|
+
includes: exec.includes
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
DEFAULT_TEXT_RECORD_BUDGET_BYTES,
|
|
67
|
+
PLACEHOLDER_CID,
|
|
68
|
+
getTextRecordBudgetBytes,
|
|
69
|
+
assertWithinBudget,
|
|
70
|
+
pessimisticSizePreflight
|
|
71
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/manifest.ts
|
|
2
|
+
var MANIFEST_VERSION = 3;
|
|
3
|
+
var MANIFEST_DIR = ".bulletin-deploy";
|
|
4
|
+
var MANIFEST_FILENAME = "manifest.json";
|
|
5
|
+
var MANIFEST_PATH = `${MANIFEST_DIR}/${MANIFEST_FILENAME}`;
|
|
6
|
+
var STABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
7
|
+
"wasm",
|
|
8
|
+
"woff",
|
|
9
|
+
"woff2",
|
|
10
|
+
"ttf",
|
|
11
|
+
"otf",
|
|
12
|
+
"eot",
|
|
13
|
+
"png",
|
|
14
|
+
"jpg",
|
|
15
|
+
"jpeg",
|
|
16
|
+
"gif",
|
|
17
|
+
"webp",
|
|
18
|
+
"avif",
|
|
19
|
+
"svg",
|
|
20
|
+
"ico",
|
|
21
|
+
"mp3",
|
|
22
|
+
"mp4",
|
|
23
|
+
"webm",
|
|
24
|
+
"ogg",
|
|
25
|
+
"pdf"
|
|
26
|
+
]);
|
|
27
|
+
var CONTENT_HASH_RE = /[-.](?:[a-f0-9]{6,16}|[A-Za-z0-9]{6,16})\.[a-zA-Z0-9]+$/;
|
|
28
|
+
function isVolatilePath(p) {
|
|
29
|
+
return p.startsWith(`${MANIFEST_DIR}/`) || p === MANIFEST_DIR;
|
|
30
|
+
}
|
|
31
|
+
function classifyFileHeuristic(filePath, framework) {
|
|
32
|
+
if (isVolatilePath(filePath)) return "volatile";
|
|
33
|
+
if (CONTENT_HASH_RE.test(filePath)) return "stable";
|
|
34
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
35
|
+
if (ext && STABLE_EXTENSIONS.has(ext)) return "stable";
|
|
36
|
+
if (framework === "next") {
|
|
37
|
+
if (filePath.startsWith("_next/static/")) return "stable";
|
|
38
|
+
}
|
|
39
|
+
if (framework === "vite") {
|
|
40
|
+
if (filePath.startsWith("assets/") && CONTENT_HASH_RE.test(filePath)) return "stable";
|
|
41
|
+
}
|
|
42
|
+
return "volatile";
|
|
43
|
+
}
|
|
44
|
+
function classifyFile(filePath, ctx = {}) {
|
|
45
|
+
if (isVolatilePath(filePath)) return "volatile";
|
|
46
|
+
const prev = ctx.prevManifest;
|
|
47
|
+
if (prev && ctx.fileCid !== void 0) {
|
|
48
|
+
const entry = prev.files[filePath];
|
|
49
|
+
if (entry && entry.cid === ctx.fileCid) return "stable";
|
|
50
|
+
return "volatile";
|
|
51
|
+
}
|
|
52
|
+
return classifyFileHeuristic(filePath, ctx.framework ?? null);
|
|
53
|
+
}
|
|
54
|
+
function parseManifest(raw) {
|
|
55
|
+
let obj;
|
|
56
|
+
try {
|
|
57
|
+
obj = JSON.parse(raw);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return { ok: false, error: `manifest JSON parse error: ${e.message}` };
|
|
60
|
+
}
|
|
61
|
+
if (!obj || typeof obj !== "object") return { ok: false, error: "manifest is not an object" };
|
|
62
|
+
if (typeof obj.version !== "number") return { ok: false, error: "manifest.version missing or not number" };
|
|
63
|
+
if (!(obj.previous_contenthash === null || typeof obj.previous_contenthash === "string")) {
|
|
64
|
+
return { ok: false, error: "manifest.previous_contenthash must be string|null" };
|
|
65
|
+
}
|
|
66
|
+
if (typeof obj.deployed_at !== "string") return { ok: false, error: "manifest.deployed_at missing" };
|
|
67
|
+
if (!obj.files || typeof obj.files !== "object") return { ok: false, error: "manifest.files missing" };
|
|
68
|
+
if (!Array.isArray(obj.stableBlockOrder)) return { ok: false, error: "manifest.stableBlockOrder missing" };
|
|
69
|
+
if (!obj.chunks || typeof obj.chunks !== "object") return { ok: false, error: "manifest.chunks missing" };
|
|
70
|
+
const chunks = {};
|
|
71
|
+
for (const [cid, raw2] of Object.entries(obj.chunks)) {
|
|
72
|
+
const r = raw2;
|
|
73
|
+
if (r && typeof r === "object") {
|
|
74
|
+
const size = typeof r.size === "number" ? r.size : 0;
|
|
75
|
+
const deployedAt = typeof r.deployed_at === "string" ? r.deployed_at : "1970-01-01T00:00:00.000Z";
|
|
76
|
+
chunks[cid] = {
|
|
77
|
+
size,
|
|
78
|
+
deployed_at: deployedAt,
|
|
79
|
+
...typeof r.block === "number" ? { block: r.block } : {},
|
|
80
|
+
...typeof r.index === "number" ? { index: r.index } : {}
|
|
81
|
+
};
|
|
82
|
+
} else {
|
|
83
|
+
chunks[cid] = { size: 0, deployed_at: "1970-01-01T00:00:00.000Z" };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const manifest = {
|
|
87
|
+
version: obj.version,
|
|
88
|
+
previous_contenthash: obj.previous_contenthash,
|
|
89
|
+
deployed_at: obj.deployed_at,
|
|
90
|
+
framework: typeof obj.framework === "string" ? obj.framework : null,
|
|
91
|
+
files: obj.files,
|
|
92
|
+
stableBlockOrder: obj.stableBlockOrder,
|
|
93
|
+
blocks: Array.isArray(obj.blocks) ? obj.blocks : [],
|
|
94
|
+
chunks
|
|
95
|
+
};
|
|
96
|
+
return { ok: true, manifest };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
MANIFEST_VERSION,
|
|
101
|
+
MANIFEST_DIR,
|
|
102
|
+
MANIFEST_FILENAME,
|
|
103
|
+
MANIFEST_PATH,
|
|
104
|
+
isVolatilePath,
|
|
105
|
+
classifyFileHeuristic,
|
|
106
|
+
classifyFile,
|
|
107
|
+
parseManifest
|
|
108
|
+
};
|