From 8ef6508645214a8465e4d289f95470f99cdaf1b0 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:25:37 +0400 Subject: [PATCH 1/7] feat: support any layout BoC --- src/decompiler/constants.ts | 9 --- src/decompiler/disasm.ts | 57 ++++++------------- src/printer/assembly-writer.ts | 22 ++++--- .../known-contracts.spec.ts.snap | 26 +++++++++ src/test/e2e/known-contracts.spec.ts | 13 ++++- 5 files changed, 69 insertions(+), 58 deletions(-) delete mode 100644 src/decompiler/constants.ts diff --git a/src/decompiler/constants.ts b/src/decompiler/constants.ts deleted file mode 100644 index 22ef91d..0000000 --- a/src/decompiler/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const RootLayout = { - instructionsCount: 4, - instructions: { - SETCP: 0, - DICTPUSHCONST: 1, - DICTIGETJMPZ: 2, - THROWARG: 3, - }, -} as const diff --git a/src/decompiler/disasm.ts b/src/decompiler/disasm.ts index 4426f9a..bf6a4fa 100644 --- a/src/decompiler/disasm.ts +++ b/src/decompiler/disasm.ts @@ -18,9 +18,8 @@ import { ProgramNode, } from "../ast/ast" import {createBlock, createInstruction} from "../ast/helpers" -import {RootLayout} from "./constants" import {getDisplayNumber, hasHint} from "../spec/helpers" -import {LayoutError, OperandError, UnknownOperandTypeError} from "./errors" +import {OperandError, UnknownOperandTypeError} from "./errors" export interface DisassembleParams { /** @@ -338,39 +337,8 @@ function processRefOrSliceOperand( } } -/** - * Checks if the root layout is valid. - * - * Valid layout is: - * - `SETCP` - * - `DICTPUSHCONST` - * - `DICTIGETJMPZ` - * - `THROWARG` - * - * This is the only layout that is supported by the decompiler. - * This layout is generated by the FunC and Tact compilers. - */ -function checkLayout(opcodes: DecompiledInstruction[]): void { - if (opcodes.length !== RootLayout.instructionsCount) { - throw new LayoutError(RootLayout.instructionsCount, opcodes.length, { - instructions: opcodes.map(op => op.op.definition.mnemonic), - }) - } - - const isValidLayout = - opcodes[RootLayout.instructions.SETCP].op.definition.mnemonic === "SETCP" && - opcodes[RootLayout.instructions.DICTPUSHCONST].op.definition.mnemonic === "DICTPUSHCONST" && - (opcodes[RootLayout.instructions.DICTIGETJMPZ].op.definition.mnemonic === "DICTIGETJMPZ" || - opcodes[RootLayout.instructions.DICTIGETJMPZ].op.definition.mnemonic === - "DICTIGETJMP") && - opcodes[RootLayout.instructions.THROWARG].op.definition.mnemonic === "THROWARG" - - if (!isValidLayout) { - throw new LayoutError(RootLayout.instructionsCount, opcodes.length, { - expected: ["SETCP", "DICTPUSHCONST", "DICTIGETJMPZ", "THROWARG"], - actual: opcodes.map(op => op.op.definition.mnemonic), - }) - } +function findDictOpcode(opcodes: DecompiledInstruction[]): DecompiledInstruction | undefined { + return opcodes.find(it => it.op.definition.mnemonic === "DICTPUSHCONST") } /** @@ -388,16 +356,27 @@ export function disassembleRoot( }, ): ProgramNode { const opcodes = disassemble({source: cell}) - checkLayout(opcodes) - - const dictOpcode = opcodes[RootLayout.instructions.DICTPUSHCONST].op - const {procedures, methods} = deserializeDict(dictOpcode.operands, options.computeRefs) const args = { source: cell, offset: {bits: 0, refs: 9}, onCellReference: undefined, } + + const dictOpcode = findDictOpcode(opcodes) + if (!dictOpcode) { + // Likely some non-Tact/FunC produced BoC + return { + type: "program", + topLevelInstructions: opcodes.map(op => processInstruction(op, args)), + procedures: [], + methods: [], + withRefs: options.computeRefs, + } + } + + const {procedures, methods} = deserializeDict(dictOpcode.op.operands, options.computeRefs) + return { type: "program", topLevelInstructions: opcodes.map(op => processInstruction(op, args)), diff --git a/src/printer/assembly-writer.ts b/src/printer/assembly-writer.ts index cb58e51..c0b739a 100644 --- a/src/printer/assembly-writer.ts +++ b/src/printer/assembly-writer.ts @@ -132,15 +132,21 @@ export class AssemblyWriter { } } - // if (node.topLevelInstructions.length > 0) { - // node.topLevelInstructions.forEach(instruction => { - // // if (i === 1) return - // this.writer.write("// ") - // this.writeInstructionNode(instruction) - // }) - // } - this.writer.writeLine(`"Asm.fif" include`) + + if (node.procedures.length === 0 && node.methods.length === 0) { + this.writer.writeLine("<{") + + this.writer.indent(() => { + node.topLevelInstructions.forEach(instruction => { + this.writeInstructionNode(instruction) + }) + }) + + this.writer.write("}>c") + return + } + this.writer.writeLine("PROGRAM{") this.writer.indent(() => { const methods = [...node.methods].sort((a, b) => a.id - b.id) diff --git a/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap b/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap index be298ed..fca4c64 100644 --- a/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap +++ b/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap @@ -1,5 +1,31 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`known contracts > should decompile Tact 1.6.0 with other layout 1`] = ` +""Asm.fif" include +PROGRAM{ + 78250 DECLMETHOD ?fun_78250 + DECLPROC ?fun_ref_92183b49329bb4e4 + ?fun_78250 PROC:<{ + PUSHROOT + CTOS + 1 LDI + DROP + <{ + NULL + }> PUSHCONT + <{ + NULL + }> PUSHCONT + IFELSE + ?fun_ref_92183b49329bb4e4 INLINECALLDICT + NIP + }> + ?fun_ref_92183b49329bb4e4 PROCREF:<{ + x{68656C6C6F20776F726C64} PUSHSLICE + }> +}END>c" +`; + exports[`known contracts > should decompile echo 1`] = ` ""Asm.fif" include PROGRAM{ diff --git a/src/test/e2e/known-contracts.spec.ts b/src/test/e2e/known-contracts.spec.ts index ae023ec..f93ec7f 100644 --- a/src/test/e2e/known-contracts.spec.ts +++ b/src/test/e2e/known-contracts.spec.ts @@ -13,7 +13,7 @@ describe("known contracts", () => { "te6cckEBAQEARAAAhP8AIN2k8mCBAgDXGCDXCx/tRNDTH9P/0VESuvKhIvkBVBBE+RDyovgAAdMfMSDXSpbTB9QC+wDe0aTIyx/L/8ntVEH98Ik=", "base64", ) - const res = decompileRaw(wallet) + const res = decompileAll(wallet) expect(res).toMatchSnapshot() // await compileFiftBackAndCompare(res, wallet, true) @@ -24,7 +24,7 @@ describe("known contracts", () => { "te6cckEBAQEAVwAAqv8AIN0gggFMl7qXMO1E0NcLH+Ck8mCDCNcYINMf0x8B+CO78mPtRNDTH9P/0VExuvKhA/kBVBBC+RDyovgAApMg10qW0wfUAvsA6NGkyMsfy//J7VShNwu2", "base64", ) - const res = decompileRaw(wallet) + const res = decompileAll(wallet) expect(res).toMatchSnapshot() // await compileFiftBackAndCompare(res, wallet) @@ -135,4 +135,13 @@ describe("known contracts", () => { // const withoutRefs = decompileAll(mathlib, debugSymbols2, false) // await compileFiftBackAndCompare(withoutRefs, mathlib) }) + + it("should decompile Tact 1.6.0 with other layout", async () => { + const wallet = Buffer.from( + "te6ccgEBAwEAXQABbP8AII4oMDDQctch0gDSAPpAIRA0UFVvBPhhAfhi7UTQ0gAwkW2RbeIw3PLAguH0pBP0vPLICwEBI6ZMartRNDSADCRbZFt4ts8MYAIAGou2hlbGxvIHdvcmxkg=", + "base64", + ) + const res = decompileAll(wallet) + expect(res).toMatchSnapshot() + }) }) From 36597453f37f29dbb3c176f5689095a8af73f04d Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:31:41 +0400 Subject: [PATCH 2/7] fix docs --- README.md | 6 +++--- src/decompiler/disasm.ts | 6 ++---- src/index.ts | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d663cb5..5a2b805 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ yarn add @tact-lang/opcode ## Usage -For most cases you will want to disassemble a BoC file generated by the Tact/FunC/Tolk compiler. +For most cases you will want to disassemble a BoC file generated by the Tact/FunC/Tolk compiler. In this case decompiler will unpack the dictionary to procedures and methods. ```typescript import {AssemblyWriter, disassembleRoot} from "@tact-lang/opcode" @@ -20,12 +20,12 @@ const program = disassembleRoot(source, { computeRefs: false, }) -// Write the program AST into a TVM bytecode string +// Write the program AST into a Fift assembly string const res = AssemblyWriter.write(program, {}) console.log(res) ``` -If you want to decompile BoC file with non-standard root layout (for example, wallet v1), you can do the following: +If you want to decompile BoC file without unpacking of the dictionary, you can do the following: ```typescript import {AssemblyWriter, disassembleRawRoot} from "@tact-lang/opcode" diff --git a/src/decompiler/disasm.ts b/src/decompiler/disasm.ts index bf6a4fa..a85ddf8 100644 --- a/src/decompiler/disasm.ts +++ b/src/decompiler/disasm.ts @@ -344,7 +344,7 @@ function findDictOpcode(opcodes: DecompiledInstruction[]): DecompiledInstruction /** * Disassembles the root cell into a list of instructions. * - * Use this function if you want to disassemble the whole BoC file. + * Use this function if you want to disassemble the whole BoC file with dictionary unpacked. */ export function disassembleRoot( cell: Cell, @@ -387,9 +387,7 @@ export function disassembleRoot( } /** - * Disassembles a cell without any additional checks for the layout. - * - * Use this function if your contract use non-usual layout. + * Disassembles a cell without any additional unpacking of the dictionary. */ export function disassembleRawRoot(cell: Cell): BlockNode { return disassembleAndProcess({ diff --git a/src/index.ts b/src/index.ts index ebe8862..406a57f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,4 +12,4 @@ export type {AssemblyWriterOptions} from "./printer/assembly-writer" export {AssemblyWriter} from "./printer/assembly-writer" export {debugSymbols} from "./utils/known-methods" -export {Cell} from "@ton/core" +export {Cell, Dictionary} from "@ton/core" From bab97842149c294debc5cc63855596cb3d7a17e5 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:32:42 +0400 Subject: [PATCH 3/7] fix --- src/decompiler/errors.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/decompiler/errors.ts b/src/decompiler/errors.ts index 9d6cb1b..c4c980d 100644 --- a/src/decompiler/errors.ts +++ b/src/decompiler/errors.ts @@ -31,17 +31,6 @@ export class OperandError extends DisassemblerError { } } -export class LayoutError extends DisassemblerError { - public constructor(expected: number, actual: number, details?: Record) { - super(`Unexpected root layout: expected ${expected} instructions, got ${actual}`, { - expected, - actual, - ...details, - }) - this.name = "LayoutError" - } -} - export class UnknownOperandTypeError extends DisassemblerError { public constructor(operand: OperandValue, details?: Record) { super(`Unknown operand type: ${operand.type}`, { From 02105d71690c8fdd3434e125812ac7fb760754bb Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:43:40 +0400 Subject: [PATCH 4/7] fix --- src/decompiler/disasm.ts | 18 ++++++++++- .../known-contracts.spec.ts.snap | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/decompiler/disasm.ts b/src/decompiler/disasm.ts index a85ddf8..57424aa 100644 --- a/src/decompiler/disasm.ts +++ b/src/decompiler/disasm.ts @@ -363,6 +363,22 @@ export function disassembleRoot( onCellReference: undefined, } + const rootMethods: MethodNode[] = [] + + if (opcodes[2]?.op.definition.mnemonic === "PUSHCONT") { + const cont = opcodes[2].op.operands[0] + if (cont.type === "subslice") { + const recvInternal = disassembleRawRoot(cont.value) + rootMethods.push({ + type: "method", + hash: recvInternal.hash, + offset: recvInternal.offset, + body: recvInternal, + id: 0, + }) + } + } + const dictOpcode = findDictOpcode(opcodes) if (!dictOpcode) { // Likely some non-Tact/FunC produced BoC @@ -381,7 +397,7 @@ export function disassembleRoot( type: "program", topLevelInstructions: opcodes.map(op => processInstruction(op, args)), procedures, - methods, + methods: [...rootMethods, ...methods], withRefs: options.computeRefs, } } diff --git a/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap b/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap index fca4c64..1c60cf3 100644 --- a/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap +++ b/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap @@ -3,8 +3,40 @@ exports[`known contracts > should decompile Tact 1.6.0 with other layout 1`] = ` ""Asm.fif" include PROGRAM{ + DECLPROC recv_internal 78250 DECLMETHOD ?fun_78250 DECLPROC ?fun_ref_92183b49329bb4e4 + recv_internal PROC:<{ + DROP + DROP + CTOS + TWO + SDSKIPFIRST + 1 LDI + 1 LDI + LDMSGADDR + OVER + s3 s4 XCHG + s5 s5 XCHG2 + 4 TUPLE + 1 SETGLOB + SWAP + 2 SETGLOB + PUSHROOT + CTOS + 1 LDI + DROP + <{ + NULL + }> PUSHCONT + <{ + NULL + }> PUSHCONT + IFELSE + DROP + IFRET + 130 THROW + }> ?fun_78250 PROC:<{ PUSHROOT CTOS From 566581605a54a23b37dc5a9077712688242f83f7 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:02:45 +0400 Subject: [PATCH 5/7] fix --- src/decompiler/disasm.ts | 54 ++++++++--- .../known-contracts.spec.ts.snap | 94 ++++++++++++++++++- src/test/e2e/known-contracts.spec.ts | 9 ++ 3 files changed, 140 insertions(+), 17 deletions(-) diff --git a/src/decompiler/disasm.ts b/src/decompiler/disasm.ts index 57424aa..2e85840 100644 --- a/src/decompiler/disasm.ts +++ b/src/decompiler/disasm.ts @@ -341,6 +341,44 @@ function findDictOpcode(opcodes: DecompiledInstruction[]): DecompiledInstruction return opcodes.find(it => it.op.definition.mnemonic === "DICTPUSHCONST") } +function findRootMethods(opcodes: DecompiledInstruction[]): MethodNode[] { + const methods: MethodNode[] = [] + + if (opcodes[2]?.op.definition.mnemonic === "PUSHCONT") { + const cont = opcodes[2].op.operands.at(0) + if (!cont || cont.type !== "subslice") { + return methods + } + + const recvInternal = disassembleRawRoot(cont.value) + methods.push({ + type: "method", + hash: recvInternal.hash, + offset: recvInternal.offset, + body: recvInternal, + id: 0, + }) + } + + if (opcodes[6]?.op.definition.mnemonic === "PUSHCONT") { + const cont = opcodes[6].op.operands.at(0) + if (!cont || cont.type !== "subslice") { + return methods + } + + const recvExternal = disassembleRawRoot(cont.value) + methods.push({ + type: "method", + hash: recvExternal.hash, + offset: recvExternal.offset, + body: recvExternal, + id: -1, + }) + } + + return methods +} + /** * Disassembles the root cell into a list of instructions. * @@ -363,21 +401,7 @@ export function disassembleRoot( onCellReference: undefined, } - const rootMethods: MethodNode[] = [] - - if (opcodes[2]?.op.definition.mnemonic === "PUSHCONT") { - const cont = opcodes[2].op.operands[0] - if (cont.type === "subslice") { - const recvInternal = disassembleRawRoot(cont.value) - rootMethods.push({ - type: "method", - hash: recvInternal.hash, - offset: recvInternal.offset, - body: recvInternal, - id: 0, - }) - } - } + const rootMethods = findRootMethods(opcodes) const dictOpcode = findDictOpcode(opcodes) if (!dictOpcode) { diff --git a/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap b/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap index 1c60cf3..831d424 100644 --- a/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap +++ b/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap @@ -3,10 +3,100 @@ exports[`known contracts > should decompile Tact 1.6.0 with other layout 1`] = ` ""Asm.fif" include PROGRAM{ - DECLPROC recv_internal + DECLPROC main 78250 DECLMETHOD ?fun_78250 DECLPROC ?fun_ref_92183b49329bb4e4 - recv_internal PROC:<{ + main PROC:<{ + DROP + DROP + CTOS + TWO + SDSKIPFIRST + 1 LDI + 1 LDI + LDMSGADDR + OVER + s3 s4 XCHG + s5 s5 XCHG2 + 4 TUPLE + 1 SETGLOB + SWAP + 2 SETGLOB + PUSHROOT + CTOS + 1 LDI + DROP + <{ + NULL + }> PUSHCONT + <{ + NULL + }> PUSHCONT + IFELSE + DROP + IFRET + 130 THROW + }> + ?fun_78250 PROC:<{ + PUSHROOT + CTOS + 1 LDI + DROP + <{ + NULL + }> PUSHCONT + <{ + NULL + }> PUSHCONT + IFELSE + ?fun_ref_92183b49329bb4e4 INLINECALLDICT + NIP + }> + ?fun_ref_92183b49329bb4e4 PROCREF:<{ + x{68656C6C6F20776F726C64} PUSHSLICE + }> +}END>c" +`; + +exports[`known contracts > should decompile Tact 1.6.0 with other layout and recv_external 1`] = ` +""Asm.fif" include +PROGRAM{ + -1 DECLMETHOD recv_external + DECLPROC main + 78250 DECLMETHOD ?fun_78250 + DECLPROC ?fun_ref_92183b49329bb4e4 + recv_external PROC:<{ + DROP + DROP + PUSHROOT + CTOS + 1 LDI + DROP + <{ + NULL + }> PUSHCONT + <{ + NULL + }> PUSHCONT + IFELSE + 1 GETGLOB + 4 UNTUPLE + s2 s3 XCHG + 3 BLKDROP + 41351 PUSHINT + MYADDR + ROT + SDEQ + THROWANYIFNOT + DROP + NEWC + -1 PUSHINT + SWAP + 1 STI + ENDC + POPROOT + }> + main PROC:<{ DROP DROP CTOS diff --git a/src/test/e2e/known-contracts.spec.ts b/src/test/e2e/known-contracts.spec.ts index f93ec7f..59554cf 100644 --- a/src/test/e2e/known-contracts.spec.ts +++ b/src/test/e2e/known-contracts.spec.ts @@ -144,4 +144,13 @@ describe("known contracts", () => { const res = decompileAll(wallet) expect(res).toMatchSnapshot() }) + + it("should decompile Tact 1.6.0 with other layout and recv_external", async () => { + const wallet = Buffer.from( + "te6ccgEBAwEAjAAByv8AII4oMDDQctch0gDSAPpAIRA0UFVvBPhhAfhi7UTQ0gAwkW2RbeIw3PLAguEgwP+OKTAw7UTQ0gAwkW2RbeL4QW8kECNfA4IAoYf4KFjHBfL0MMh/AcoAye1U4PSkE/S88sgLAQEjpkxqu1E0NIAMJFtkW3i2zwxgAgAai7aGVsbG8gd29ybGSA==", + "base64", + ) + const res = decompileAll(wallet) + expect(res).toMatchSnapshot() + }) }) From 3deb012ce700143e7e57a023850577a5f0dce28d Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:07:23 +0400 Subject: [PATCH 6/7] fix --- src/test/e2e/__snapshots__/known-contracts.spec.ts.snap | 8 ++++---- src/test/e2e/known-contracts.spec.ts | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap b/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap index 831d424..0eb882a 100644 --- a/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap +++ b/src/test/e2e/__snapshots__/known-contracts.spec.ts.snap @@ -3,10 +3,10 @@ exports[`known contracts > should decompile Tact 1.6.0 with other layout 1`] = ` ""Asm.fif" include PROGRAM{ - DECLPROC main + DECLPROC recv_internal 78250 DECLMETHOD ?fun_78250 DECLPROC ?fun_ref_92183b49329bb4e4 - main PROC:<{ + recv_internal PROC:<{ DROP DROP CTOS @@ -62,7 +62,7 @@ exports[`known contracts > should decompile Tact 1.6.0 with other layout and rec ""Asm.fif" include PROGRAM{ -1 DECLMETHOD recv_external - DECLPROC main + DECLPROC recv_internal 78250 DECLMETHOD ?fun_78250 DECLPROC ?fun_ref_92183b49329bb4e4 recv_external PROC:<{ @@ -96,7 +96,7 @@ PROGRAM{ ENDC POPROOT }> - main PROC:<{ + recv_internal PROC:<{ DROP DROP CTOS diff --git a/src/test/e2e/known-contracts.spec.ts b/src/test/e2e/known-contracts.spec.ts index 59554cf..2f87e2a 100644 --- a/src/test/e2e/known-contracts.spec.ts +++ b/src/test/e2e/known-contracts.spec.ts @@ -115,11 +115,10 @@ describe("known contracts", () => { it("should decompile mathlib.fc", async () => { const debugSymbols2: DebugSymbols = { - procedures: [], - globals: [], - constants: [], + procedures: [...debugSymbols.procedures], + globals: [...debugSymbols.globals], + constants: [...debugSymbols.constants], } - Object.assign(debugSymbols2, debugSymbols) debugSymbols2.procedures[0] = { methodId: 0, From 9b4bfcfb352089b4c3c542d33b54d05745452227 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:10:16 +0400 Subject: [PATCH 7/7] fix --- src/decompiler/disasm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/decompiler/disasm.ts b/src/decompiler/disasm.ts index 2e85840..e802bc1 100644 --- a/src/decompiler/disasm.ts +++ b/src/decompiler/disasm.ts @@ -410,7 +410,7 @@ export function disassembleRoot( type: "program", topLevelInstructions: opcodes.map(op => processInstruction(op, args)), procedures: [], - methods: [], + methods: rootMethods, withRefs: options.computeRefs, } }