cashscript 0.11.3 → 0.11.5
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/dist/Transaction.js +1 -2
- package/dist/TransactionBuilder.js +6 -3
- package/dist/advanced/LibauthTemplate.js +1 -1
- package/dist/debugging.js +11 -3
- package/dist/network/MockNetworkProvider.d.ts +8 -3
- package/dist/network/MockNetworkProvider.js +41 -13
- package/dist/types/type-inference.d.ts +8 -6
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +13 -30
- package/package.json +5 -6
package/dist/Transaction.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { hexToBin, decodeTransaction, } from '@bitauth/libauth';
|
|
2
|
-
import delay from 'delay';
|
|
3
2
|
import { encodeBip68, placeholder, } from '@cashscript/utils';
|
|
4
3
|
import deepEqual from 'fast-deep-equal';
|
|
5
4
|
import { isUtxoP2PKH, SignatureAlgorithm, } from './interfaces.js';
|
|
6
|
-
import { createInputScript, getInputSize, createOpReturnOutput, getTxSizeWithoutInputs, validateOutput, utxoComparator, calculateDust, getOutputSize, utxoTokenComparator, } from './utils.js';
|
|
5
|
+
import { createInputScript, getInputSize, createOpReturnOutput, getTxSizeWithoutInputs, validateOutput, utxoComparator, calculateDust, getOutputSize, utxoTokenComparator, delay, } from './utils.js';
|
|
7
6
|
import SignatureTemplate from './SignatureTemplate.js';
|
|
8
7
|
import { P2PKH_INPUT_SIZE } from './constants.js';
|
|
9
8
|
import { TransactionBuilder } from './TransactionBuilder.js';
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { binToHex, decodeTransaction, decodeTransactionUnsafe, encodeTransaction, hexToBin, } from '@bitauth/libauth';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { cashScriptOutputToLibauthOutput, createOpReturnOutput, generateLibauthSourceOutputs, validateInput, validateOutput, } from './utils.js';
|
|
2
|
+
import { isUnlockableUtxo, isStandardUnlockableUtxo, isP2PKHUnlocker, } from './interfaces.js';
|
|
3
|
+
import { cashScriptOutputToLibauthOutput, createOpReturnOutput, delay, generateLibauthSourceOutputs, validateInput, validateOutput, } from './utils.js';
|
|
5
4
|
import { FailedTransactionError } from './Errors.js';
|
|
6
5
|
import { getBitauthUri } from './LibauthTemplate.js';
|
|
7
6
|
import { debugLibauthTemplate, getLibauthTemplates } from './advanced/LibauthTemplate.js';
|
|
@@ -90,6 +89,10 @@ export class TransactionBuilder {
|
|
|
90
89
|
return binToHex(encodeTransaction(transaction));
|
|
91
90
|
}
|
|
92
91
|
debug() {
|
|
92
|
+
// do not debug a pure P2PKH-spend transaction
|
|
93
|
+
if (this.inputs.every((input) => isP2PKHUnlocker(input.unlocker))) {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
93
96
|
if (this.inputs.some((input) => !isStandardUnlockableUtxo(input))) {
|
|
94
97
|
throw new Error('Cannot debug a transaction with custom unlocker');
|
|
95
98
|
}
|
|
@@ -360,7 +360,7 @@ export const getLibauthTemplates = (txn) => {
|
|
|
360
360
|
export const debugLibauthTemplate = (template, transaction) => {
|
|
361
361
|
const allArtifacts = transaction.inputs
|
|
362
362
|
.map(input => 'contract' in input.unlocker ? input.unlocker.contract : undefined)
|
|
363
|
-
.filter((contract) =>
|
|
363
|
+
.filter((contract) => Boolean(contract))
|
|
364
364
|
.map(contract => contract.artifact);
|
|
365
365
|
return debugTemplate(template, allArtifacts);
|
|
366
366
|
};
|
package/dist/debugging.js
CHANGED
|
@@ -41,7 +41,8 @@ const debugSingleScenario = (template, artifact, unlockingScriptId, scenarioId)
|
|
|
41
41
|
const executedLogs = (artifact.debug?.logs ?? [])
|
|
42
42
|
.filter((log) => executedDebugSteps.some((debugStep) => log.ip === debugStep.ip));
|
|
43
43
|
for (const log of executedLogs) {
|
|
44
|
-
|
|
44
|
+
const inputIndex = extractInputIndexFromScenario(scenarioId);
|
|
45
|
+
logConsoleLogStatement(log, executedDebugSteps, artifact, inputIndex);
|
|
45
46
|
}
|
|
46
47
|
const lastExecutedDebugStep = executedDebugSteps[executedDebugSteps.length - 1];
|
|
47
48
|
// If an error is present in the last step, that means a require statement in the middle of the function failed
|
|
@@ -91,6 +92,13 @@ const debugSingleScenario = (template, artifact, unlockingScriptId, scenarioId)
|
|
|
91
92
|
}
|
|
92
93
|
return fullDebugSteps;
|
|
93
94
|
};
|
|
95
|
+
// Note: this relies on the naming convention that the scenario ID is of the form <name>_input<index>_evaluate
|
|
96
|
+
const extractInputIndexFromScenario = (scenarioId) => {
|
|
97
|
+
const match = scenarioId.match(/_input(\d+)_/);
|
|
98
|
+
if (!match)
|
|
99
|
+
throw new Error(`Invalid scenario ID: ${scenarioId}`);
|
|
100
|
+
return parseInt(match[1]);
|
|
101
|
+
};
|
|
94
102
|
// internal util. instantiates the virtual machine and compiles the template into a program
|
|
95
103
|
const createProgram = (template, unlockingScriptId, scenarioId) => {
|
|
96
104
|
const configuration = walletTemplateToCompilerConfiguration(template);
|
|
@@ -115,7 +123,7 @@ const createProgram = (template, unlockingScriptId, scenarioId) => {
|
|
|
115
123
|
}
|
|
116
124
|
return { vm, program: scenarioGeneration.scenario.program };
|
|
117
125
|
};
|
|
118
|
-
const logConsoleLogStatement = (log, debugSteps, artifact) => {
|
|
126
|
+
const logConsoleLogStatement = (log, debugSteps, artifact, inputIndex) => {
|
|
119
127
|
let line = `${artifact.contractName}.cash:${log.line}`;
|
|
120
128
|
const decodedData = log.data.map((element) => {
|
|
121
129
|
if (typeof element === 'string')
|
|
@@ -124,7 +132,7 @@ const logConsoleLogStatement = (log, debugSteps, artifact) => {
|
|
|
124
132
|
const transformedDebugStep = applyStackItemTransformations(element, debugStep);
|
|
125
133
|
return decodeStackItem(element, transformedDebugStep.stack);
|
|
126
134
|
});
|
|
127
|
-
console.log(
|
|
135
|
+
console.log(`[Input #${inputIndex}] ${line} ${decodedData.join(' ')}`);
|
|
128
136
|
};
|
|
129
137
|
const applyStackItemTransformations = (element, debugStep) => {
|
|
130
138
|
if (!element.transformations)
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { Utxo, Network } from '../interfaces.js';
|
|
2
2
|
import NetworkProvider from './NetworkProvider.js';
|
|
3
|
+
interface MockNetworkProviderOptions {
|
|
4
|
+
updateUtxoSet: boolean;
|
|
5
|
+
}
|
|
3
6
|
export default class MockNetworkProvider implements NetworkProvider {
|
|
4
|
-
private
|
|
7
|
+
private utxoSet;
|
|
5
8
|
private transactionMap;
|
|
6
9
|
network: Network;
|
|
7
10
|
blockHeight: number;
|
|
8
|
-
|
|
11
|
+
options: MockNetworkProviderOptions;
|
|
12
|
+
constructor(options?: Partial<MockNetworkProviderOptions>);
|
|
9
13
|
getUtxos(address: string): Promise<Utxo[]>;
|
|
10
14
|
setBlockHeight(newBlockHeight: number): void;
|
|
11
15
|
getBlockHeight(): Promise<number>;
|
|
12
16
|
getRawTransaction(txid: string): Promise<string>;
|
|
13
17
|
sendRawTransaction(txHex: string): Promise<string>;
|
|
14
|
-
addUtxo(
|
|
18
|
+
addUtxo(addressOrLockingBytecode: string, utxo: Utxo): void;
|
|
15
19
|
reset(): void;
|
|
16
20
|
}
|
|
21
|
+
export {};
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { binToHex, hexToBin } from '@bitauth/libauth';
|
|
1
|
+
import { binToHex, decodeTransactionUnsafe, hexToBin, isHex } from '@bitauth/libauth';
|
|
2
2
|
import { sha256 } from '@cashscript/utils';
|
|
3
3
|
import { Network } from '../interfaces.js';
|
|
4
|
-
import { addressToLockScript, randomUtxo } from '../utils.js';
|
|
4
|
+
import { addressToLockScript, libauthTokenDetailsToCashScriptTokenDetails, randomUtxo } from '../utils.js';
|
|
5
5
|
// redeclare the addresses from vars.ts instead of importing them
|
|
6
6
|
const aliceAddress = 'bchtest:qpgjmwev3spwlwkgmyjrr2s2cvlkkzlewq62mzgjnp';
|
|
7
7
|
const bobAddress = 'bchtest:qz6q5gqnxdldkr07xpls5474mmzmlesd6qnux4skuc';
|
|
8
8
|
const carolAddress = 'bchtest:qqsr7nqwe6rq5crj63gy5gdqchpnwmguusmr7tfmsj';
|
|
9
|
+
// We are setting the default updateUtxoSet to 'false' so that it doesn't break the current behaviour
|
|
10
|
+
// TODO: in a future breaking release we want to set this to 'true' by default
|
|
9
11
|
export default class MockNetworkProvider {
|
|
10
|
-
constructor() {
|
|
11
|
-
|
|
12
|
+
constructor(options) {
|
|
13
|
+
// we use lockingBytecode hex as the key for utxoMap to make cash addresses and token addresses interchangeable
|
|
14
|
+
this.utxoSet = [];
|
|
12
15
|
this.transactionMap = {};
|
|
13
16
|
this.network = Network.MOCKNET;
|
|
14
17
|
this.blockHeight = 133700;
|
|
18
|
+
this.options = { updateUtxoSet: false, ...options };
|
|
15
19
|
for (let i = 0; i < 3; i += 1) {
|
|
16
20
|
this.addUtxo(aliceAddress, randomUtxo());
|
|
17
21
|
this.addUtxo(bobAddress, randomUtxo());
|
|
@@ -19,8 +23,8 @@ export default class MockNetworkProvider {
|
|
|
19
23
|
}
|
|
20
24
|
}
|
|
21
25
|
async getUtxos(address) {
|
|
22
|
-
const
|
|
23
|
-
return this.
|
|
26
|
+
const addressLockingBytecode = binToHex(addressToLockScript(address));
|
|
27
|
+
return this.utxoSet.filter(([lockingBytecode]) => lockingBytecode === addressLockingBytecode).map(([, utxo]) => utxo);
|
|
24
28
|
}
|
|
25
29
|
setBlockHeight(newBlockHeight) {
|
|
26
30
|
this.blockHeight = newBlockHeight;
|
|
@@ -34,18 +38,42 @@ export default class MockNetworkProvider {
|
|
|
34
38
|
async sendRawTransaction(txHex) {
|
|
35
39
|
const transactionBin = hexToBin(txHex);
|
|
36
40
|
const txid = binToHex(sha256(sha256(transactionBin)).reverse());
|
|
41
|
+
if (this.options.updateUtxoSet && this.transactionMap[txid]) {
|
|
42
|
+
throw new Error(`Transaction with txid ${txid} was already submitted`);
|
|
43
|
+
}
|
|
37
44
|
this.transactionMap[txid] = txHex;
|
|
45
|
+
// If updateUtxoSet is false, we don't need to update the utxo set, and just return the txid
|
|
46
|
+
if (!this.options.updateUtxoSet)
|
|
47
|
+
return txid;
|
|
48
|
+
const decodedTransaction = decodeTransactionUnsafe(transactionBin);
|
|
49
|
+
decodedTransaction.inputs.forEach((input) => {
|
|
50
|
+
const utxoIndex = this.utxoSet.findIndex(([, utxo]) => utxo.txid === binToHex(input.outpointTransactionHash) && utxo.vout === input.outpointIndex);
|
|
51
|
+
// TODO: we should check what error a BCHN node throws, so we can throw the same error here
|
|
52
|
+
if (utxoIndex === -1) {
|
|
53
|
+
throw new Error(`UTXO not found for input ${input.outpointIndex} of transaction ${txid}`);
|
|
54
|
+
}
|
|
55
|
+
this.utxoSet.splice(utxoIndex, 1);
|
|
56
|
+
});
|
|
57
|
+
decodedTransaction.outputs.forEach((output, vout) => {
|
|
58
|
+
this.addUtxo(binToHex(output.lockingBytecode), {
|
|
59
|
+
txid,
|
|
60
|
+
vout,
|
|
61
|
+
satoshis: output.valueSatoshis,
|
|
62
|
+
token: output.token && libauthTokenDetailsToCashScriptTokenDetails(output.token),
|
|
63
|
+
});
|
|
64
|
+
});
|
|
38
65
|
return txid;
|
|
39
66
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
67
|
+
// Note: the user can technically add the same UTXO multiple times (txid + vout), to the same or different addresses
|
|
68
|
+
// but we don't check for this in the sendRawTransaction method. We might want to prevent duplicates from being added
|
|
69
|
+
// in the first place.
|
|
70
|
+
addUtxo(addressOrLockingBytecode, utxo) {
|
|
71
|
+
const lockingBytecode = isHex(addressOrLockingBytecode) ?
|
|
72
|
+
addressOrLockingBytecode : binToHex(addressToLockScript(addressOrLockingBytecode));
|
|
73
|
+
this.utxoSet.push([lockingBytecode, utxo]);
|
|
46
74
|
}
|
|
47
75
|
reset() {
|
|
48
|
-
this.
|
|
76
|
+
this.utxoSet = [];
|
|
49
77
|
this.transactionMap = {};
|
|
50
78
|
}
|
|
51
79
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type SignatureTemplate from '../SignatureTemplate.js';
|
|
2
|
+
type BytesType = Uint8Array | string;
|
|
3
|
+
type SignatureType = SignatureTemplate | BytesType;
|
|
2
4
|
type TypeMap = {
|
|
3
|
-
[k: `bytes${number}`]:
|
|
5
|
+
[k: `bytes${number}`]: BytesType;
|
|
4
6
|
} & {
|
|
5
|
-
byte:
|
|
6
|
-
bytes:
|
|
7
|
+
byte: BytesType;
|
|
8
|
+
bytes: BytesType;
|
|
7
9
|
bool: boolean;
|
|
8
10
|
int: bigint;
|
|
9
11
|
string: string;
|
|
10
|
-
pubkey:
|
|
11
|
-
sig:
|
|
12
|
-
datasig:
|
|
12
|
+
pubkey: BytesType;
|
|
13
|
+
sig: SignatureType;
|
|
14
|
+
datasig: BytesType;
|
|
13
15
|
};
|
|
14
16
|
type ProcessParam<Param> = Param extends {
|
|
15
17
|
type: infer Type;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Transaction } from '@bitauth/libauth';
|
|
2
2
|
import { Script } from '@cashscript/utils';
|
|
3
|
-
import { Utxo, Output, LibauthOutput, TokenDetails, AddressType, UnlockableUtxo } from './interfaces.js';
|
|
3
|
+
import { Utxo, Output, LibauthOutput, TokenDetails, AddressType, UnlockableUtxo, LibauthTokenDetails } from './interfaces.js';
|
|
4
4
|
export declare function validateInput(utxo: Utxo): void;
|
|
5
5
|
export declare function validateOutput(output: Output): void;
|
|
6
6
|
export declare function calculateDust(output: Output): number;
|
|
@@ -8,6 +8,7 @@ export declare function getOutputSize(output: Output): number;
|
|
|
8
8
|
export declare function encodeOutput(output: Output): Uint8Array;
|
|
9
9
|
export declare function cashScriptOutputToLibauthOutput(output: Output): LibauthOutput;
|
|
10
10
|
export declare function libauthOutputToCashScriptOutput(output: LibauthOutput): Output;
|
|
11
|
+
export declare function libauthTokenDetailsToCashScriptTokenDetails(token: LibauthTokenDetails): TokenDetails;
|
|
11
12
|
export declare function generateLibauthSourceOutputs(inputs: UnlockableUtxo[]): LibauthOutput[];
|
|
12
13
|
export declare function getInputSize(inputScript: Uint8Array): number;
|
|
13
14
|
export declare function getTxSizeWithoutInputs(outputs: Output[]): number;
|
|
@@ -37,3 +38,4 @@ export declare const extendedStringify: (obj: any, spaces?: number) => string;
|
|
|
37
38
|
export declare const zip: <T, U>(a: readonly T[], b: readonly U[]) => [T, U][];
|
|
38
39
|
export declare const isFungibleTokenUtxo: (utxo: Utxo) => boolean;
|
|
39
40
|
export declare const isNonTokenUtxo: (utxo: Utxo) => boolean;
|
|
41
|
+
export declare const delay: (ms: number) => Promise<void>;
|
package/dist/utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { cashAddressToLockingBytecode, decodeCashAddress, addressContentsToLockingBytecode, lockingBytecodeToCashAddress, binToHex, generateSigningSerializationBch, utf8ToBin, hexToBin,
|
|
2
|
-
import { encodeInt, hash160, hash256, sha256, Op, scriptToBytecode, } from '@cashscript/utils';
|
|
1
|
+
import { cashAddressToLockingBytecode, decodeCashAddress, addressContentsToLockingBytecode, lockingBytecodeToCashAddress, binToHex, generateSigningSerializationBch, utf8ToBin, hexToBin, LockingBytecodeType, encodeTransactionOutput, isHex, bigIntToCompactUint, NonFungibleTokenCapability, bigIntToVmNumber, } from '@bitauth/libauth';
|
|
2
|
+
import { encodeInt, hash160, hash256, sha256, Op, scriptToBytecode, encodeNullDataScript, } from '@cashscript/utils';
|
|
3
3
|
import { Network, } from './interfaces.js';
|
|
4
4
|
import { VERSION_SIZE, LOCKTIME_SIZE } from './constants.js';
|
|
5
5
|
import { OutputSatoshisTooSmallError, OutputTokenAmountTooSmallError, TokensToNonTokenAddressError, UndefinedInputError, } from './Errors.js';
|
|
@@ -64,13 +64,16 @@ export function libauthOutputToCashScriptOutput(output) {
|
|
|
64
64
|
return {
|
|
65
65
|
to: output.lockingBytecode,
|
|
66
66
|
amount: output.valueSatoshis,
|
|
67
|
-
token: output.token &&
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
token: output.token && libauthTokenDetailsToCashScriptTokenDetails(output.token),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function libauthTokenDetailsToCashScriptTokenDetails(token) {
|
|
71
|
+
return {
|
|
72
|
+
...token,
|
|
73
|
+
category: binToHex(token.category),
|
|
74
|
+
nft: token.nft && {
|
|
75
|
+
...token.nft,
|
|
76
|
+
commitment: binToHex(token.nft.commitment),
|
|
74
77
|
},
|
|
75
78
|
};
|
|
76
79
|
}
|
|
@@ -217,27 +220,6 @@ export function getNetworkPrefix(network) {
|
|
|
217
220
|
return 'bitcoincash';
|
|
218
221
|
}
|
|
219
222
|
}
|
|
220
|
-
// ////////////////////////////////////////////////////////////////////////////
|
|
221
|
-
// For encoding OP_RETURN data (doesn't require BIP62.3 / MINIMALDATA)
|
|
222
|
-
function encodeNullDataScript(chunks) {
|
|
223
|
-
return flattenBinArray(chunks.map((chunk) => {
|
|
224
|
-
if (typeof chunk === 'number') {
|
|
225
|
-
return new Uint8Array([chunk]);
|
|
226
|
-
}
|
|
227
|
-
const pushdataOpcode = getPushDataOpcode(chunk);
|
|
228
|
-
return new Uint8Array([...pushdataOpcode, ...chunk]);
|
|
229
|
-
}));
|
|
230
|
-
}
|
|
231
|
-
function getPushDataOpcode(data) {
|
|
232
|
-
const { byteLength } = data;
|
|
233
|
-
if (byteLength === 0)
|
|
234
|
-
return Uint8Array.from([0x4c, 0x00]);
|
|
235
|
-
if (byteLength < 76)
|
|
236
|
-
return Uint8Array.from([byteLength]);
|
|
237
|
-
if (byteLength < 256)
|
|
238
|
-
return Uint8Array.from([0x4c, byteLength]);
|
|
239
|
-
throw new Error('Pushdata too large');
|
|
240
|
-
}
|
|
241
223
|
const randomInt = () => BigInt(Math.floor(Math.random() * 10000));
|
|
242
224
|
export const randomUtxo = (defaults) => ({
|
|
243
225
|
...{
|
|
@@ -287,4 +269,5 @@ export const extendedStringify = (obj, spaces) => JSON.stringify(obj, (_, v) =>
|
|
|
287
269
|
export const zip = (a, b) => (Array.from(Array(Math.max(b.length, a.length)), (_, i) => [a[i], b[i]]));
|
|
288
270
|
export const isFungibleTokenUtxo = (utxo) => (utxo.token !== undefined && utxo.token.amount > 0n && utxo.token.nft === undefined);
|
|
289
271
|
export const isNonTokenUtxo = (utxo) => utxo.token === undefined;
|
|
272
|
+
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
290
273
|
//# sourceMappingURL=utils.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cashscript",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"description": "Easily write and interact with Bitcoin Cash contracts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bitcoin cash",
|
|
@@ -46,13 +46,12 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@bitauth/libauth": "^3.1.0-next.2",
|
|
49
|
-
"@cashscript/utils": "^0.11.
|
|
49
|
+
"@cashscript/utils": "^0.11.5",
|
|
50
50
|
"@electrum-cash/network": "^4.1.3",
|
|
51
51
|
"@mr-zwets/bchn-api-wrapper": "^1.0.1",
|
|
52
|
-
"delay": "^6.0.0",
|
|
53
52
|
"fast-deep-equal": "^3.1.3",
|
|
54
53
|
"pako": "^2.1.0",
|
|
55
|
-
"semver": "^7.
|
|
54
|
+
"semver": "^7.7.2"
|
|
56
55
|
},
|
|
57
56
|
"devDependencies": {
|
|
58
57
|
"@jest/globals": "^29.7.0",
|
|
@@ -61,7 +60,7 @@
|
|
|
61
60
|
"@types/semver": "^7.5.8",
|
|
62
61
|
"eslint": "^8.54.0",
|
|
63
62
|
"jest": "^29.7.0",
|
|
64
|
-
"typescript": "^5.
|
|
63
|
+
"typescript": "^5.9.2"
|
|
65
64
|
},
|
|
66
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "0ccda722dde4c0825285f3a6aa05898a8f62c160"
|
|
67
66
|
}
|