[
  {
    "path": ".github/workflows/go.yml",
    "content": "name: Go\n\non:\n  push:\n    branches: [ \"master\" ]\n  workflow_dispatch: {}\n\njobs:\n  build:\n    runs-on: ubuntu-24.04\n    environment: TESTS\n    services:\n      postgres:\n        image: postgres\n        env:\n          POSTGRES_DB: payment_processor\n          POSTGRES_USER: pp_user\n          POSTGRES_PASSWORD: postgres\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        ports:\n          - 5432:5432\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.\n          fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.\n\n      - name: Set up Go\n        uses: actions/setup-go@v3\n        with:\n          go-version: 1.24.0\n\n      - name: Build\n        run: go build -v ./...\n\n      - uses: actions/cache@v4\n        with:\n          path: ~/go/pkg/mod\n          key: ${{ runner.os }}-2go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ runner.os }}-2go-\n\n      - name: Install dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get -y install openssl ca-certificates libsodium23\n\n      - name: Install libs\n        run: |\n          wget -O libemulator.so https://github.com/ton-blockchain/ton/releases/download/v2025.06/libemulator-linux-x86_64.so\n          sudo cp libemulator.so /lib\n\n      - name: Update Library Cache\n        run: sudo ldconfig\n\n      - name: Verify Library Presence\n        run: ls /lib | grep libemulator.so\n\n      - name: Run Test\n        env:\n          SEED: ${{ secrets.SEED }}\n          SERVER: ${{ secrets.SERVER }}\n          KEY: ${{ secrets.KEY }}\n          DB_URI: postgresql://pp_user:postgres@localhost:5432/payment_processor?sslmode=disable\n        run: |\n          go test -v $(go list ./...)"
  },
  {
    "path": "Dockerfile",
    "content": "FROM docker.io/library/golang:1.24-bookworm AS builder\nWORKDIR /build-dir\nCOPY go.mod .\nCOPY go.sum .\nRUN go mod download all\nCOPY api api\nCOPY blockchain blockchain\nCOPY cmd cmd\nCOPY config config\nCOPY core core\nCOPY db db\nCOPY audit audit\nCOPY queue queue\nCOPY webhook webhook\nCOPY metrics metrics\nRUN apt-get update && apt-get install -y libsodium23\nARG GIT_TAG\nRUN go build -ldflags \"-X main.Version=$GIT_TAG\" -o /tmp/processor github.com/gobicycle/bicycle/cmd/processor\nRUN go build -ldflags \"-X main.Version=$GIT_TAG\" -o /tmp/testutil github.com/gobicycle/bicycle/cmd/testutil\n\nFROM docker.io/library/ubuntu:24.04 AS payment-processor\nRUN apt-get update && apt-get install -y openssl ca-certificates libsodium23 wget && rm -rf /var/lib/apt/lists/*\nRUN mkdir -p /app/lib\nCOPY --from=builder /go/pkg/mod/github.com/tonkeeper/tongo*/lib/linux /app/lib/\nENV LD_LIBRARY_PATH=/app/lib\nCOPY --from=builder /tmp/processor /app/processor\nCMD [\"/app/processor\", \"-v\"]\n\nFROM docker.io/library/ubuntu:24.04 AS payment-test\nRUN apt-get update && apt-get install -y openssl ca-certificates libsodium23 wget && rm -rf /var/lib/apt/lists/*\nRUN mkdir -p /app/lib\nCOPY --from=builder /go/pkg/mod/github.com/tonkeeper/tongo*/lib/linux /app/lib/\nENV LD_LIBRARY_PATH=/app/lib\nCOPY --from=builder /tmp/testutil /app/testutil\nCMD [\"/app/testutil\", \"-v\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "Makefile",
    "content": "VERSION := latest\nGIT_TAG := $(shell git describe --tags --always)\n\nbuild:\n\t@echo \"Building tag: $(GIT_TAG)\"\n\tdocker build --build-arg GIT_TAG=$(GIT_TAG) -t payment-processor:$(VERSION) --target payment-processor .\n\tdocker build --build-arg GIT_TAG=$(GIT_TAG) -t payment-test:$(VERSION) --target payment-test ."
  },
  {
    "path": "README.md",
    "content": "# TON payment processor\n[![Based on TON][ton-svg]][ton]\n[![Go](https://github.com/gobicycle/bicycle/actions/workflows/go.yml/badge.svg)](https://github.com/gobicycle/bicycle/actions/workflows/go.yml)\n[![Telegram][telegram-svg]][telegram-url]\n\nMicroservice for accepting payments and making withdrawals to wallets in TON blockchain.  \nSupports TON coins and Jettons (conforming certain criteria)  \nProvides REST API for integration.\nService is ADNL based and interacts directly with node and do not use any third party API.\n\n**Warning** Manual withdrawals from hot wallet is prohibited **do not withdraw funds from a hot wallet bypassing the service, this will lead to a service error**\n\n- [How it works](#How-it-works)\n  - [Features](#Features)\n- [Glossary](#Glossary)\n- [Prerequisites](#Prerequisites)\n  - [Criteria for valid Jettons](#Criteria-for-valid-Jettons)\n- [Deployment](#Deployment)\n  - [Configurable parameters](#Configurable-parameters)\n  - [Service deploy](#Service-deploy)\n- [Payment notifications](#Payment-notifications)\n- [Binary comment support](#Binary-comment-support)\n- [REST API](https://gobicycle.github.io/bicycle/)\n- [Technical notes](/technical_notes.md)\n- [Threat model](/threat_model.md)\n- [Manual migrations](/manual_migrations.md)\n- [TODO list](/todo_list.md)\n\n![test_dashboard](https://user-images.githubusercontent.com/120649456/211955983-698b12b8-eccf-45c5-85bb-f8f6364c154e.png)\n![db_dashboard](https://user-images.githubusercontent.com/120649456/211955998-749772a6-10d2-4594-96f1-be6ab6051be5.png)\n\n## How it works\n\nThe service provides the following functionality:\n\n- `generate new deposit address` of wallet (TON or Jetton) for TON blockchain. This address you provide to customer for payment. Payments are accumulated in the hot-wallet.\n- `make withdrawal` of TONs or Jettons from hot-wallet to customer wallet at TON blockchain.\n\n### Features\n* Deposit addresses can be reused for multiple payments\n* Sends withdrawals with comment\n* Sends withdrawals in batches using highload wallet\n* Aggregates part of TONs or Jettons at cold-wallet\n* Supports authorization by Bearer token\n* Service withdrawals (cancellation of incorrect payments)\n\n## Glossary\n\n- `deposit-address` - address generated by the service to which users send payments.\n- `deposit` - blockchain account with `deposit-address`\n- `hot-wallet` - wallet for aggregation all incoming TONs and Jettons from deposit-addresses.\n- `cold-wallet` - wallet to which part of the funds from the hot wallet is sent for security. Cold-wallet seed phrase is not used by the service.\n- `user_id` - unique text value to identify deposit-addresses or withdrawal request for a specific user.\n- `query_id` - unique text value to identify withdrawal request for a specific user to prevent double spending.\n- `basic unit` - minimum indivisible unit for TON (e.g. for TON `basic unit` = nanoTONs) or Jetton.\n- `hot_wallet_minimum_balance` - minimum TON balance in nanoTONs at hot wallet to start service.\n- `hot_wallet_maximum_balance` - maximum balance (of TONs or Jettons) in basic units at hot wallet. Anything more than this amount will be withdrawn to a cold wallet.\n- `minimum_withdrawal_amount` - minimum balance (of TONs or Jettons) in basic units at deposit account to make withdrawal to hot wallet. It is necessary to prevent the case when the withdrawal fee will be close to the balance on the deposit.\n\n## Prerequisites\n- Need minimum (configured) amount of TONs at HighloadV2 wallet address correlated with seed phrase or already deployed HighloadV2 wallet. \n- To ensure the reliability and security of the service, you need to provide your own TON node (with lite server) on the same machine as the service. If you want to use an untrusted node (such as a rented node), you need to set `PROOF_CHECK_ENABLED=true` and specify `NETWORK_CONFIG_URL`.\n- Jettons used must meet certain criteria\n\n### Criteria for valid Jettons\n- conforming to the standard [TEP-74](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md)\n- the Jetton wallet should not spontaneously change its balance, only with transfer.\n- fee for the withdrawal of Jettons from the wallet should not be too high and meet the internal setting of the service\n\nFor more information on Jettons compatibility, see [Jettons compatibility](/jettons.md)\n\n## Deployment\n\n### Configurable parameters\n| ENV variable                 | Description                                                                                                                                                                                                                                                                                                                                                                   |\n|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `LITESERVER`                 | IP and port of lite server, example: `185.86.76.183:5815`                                                                                                                                                                                                                                                                                                                     |\n| `LITESERVER_KEY`             | public key of lite server `5v2dHtSclsGsZVbNVwTj4hQDso5xvQjzL/yPEHJevHk=`. <br/>Be careful with base64 encoding and ENV var. Use ''                                                                                                                                                                                                                                            |\n| `LITESERVER_RATE_LIMIT`      | If you have a rented node with an RPS limit, set the RPS value here equal to (or preferably slightly less than) the limit. Default: 100.                                                                                                                                                                                                                                      |\n| `SEED`                       | seed phrase for main hot wallet. 24 words compatible with standard TON wallets                                                                                                                                                                                                                                                                                                |\n| `DB_URI`                     | URI for DB connection, example: <br/>`postgresql://db_user:db_password@localhost:5432/payment_processor`                                                                                                                                                                                                                                                                      |\n| `POSTGRES_DB`                | name of database for storing payments data                                                                                                                                                                                                                                                                                                                                    |\n| `POSTGRES_READONLY_PASSWORD` | password for grafana readonly db user                                                                                                                                                                                                                                                                                                                                         |\n| `API_PORT`                   | port for REST API, example `8081`                                                                                                                                                                                                                                                                                                                                             |\n| `API_TOKEN`                  | Bearer token for REST API, example `123`                                                                                                                                                                                                                                                                                                                                      |\n| `IS_TESTNET`                 | `true` if service works in TESTNET, `false` - for MAINNET. Default: `true`.                                                                                                                                                                                                                                                                                                   |\n| `JETTONS`                    | list of Jettons, processed by service in format: <br/>`JETTON_SYMBOL_1:MASTER_CONTRACT_ADDR_1:hot_wallet_max_balance:min_withdrawal_amount:hot_wallet_residual_balance, JETTON_SYMBOL_2:MASTER_CONTRACT_ADDR_2:hot_wallet_max_balance:min_withdrawal_amount:hot_wallet_residual_balance`, <br/>example: `TGR:kQCKt2WPGX-fh0cIAz38Ljd_OKQjoZE_cqk7QrYGsNP6wfP0:1000000:100000` |\n| `TON_CUTOFFS`                | cutoffs in nanoTONs in format: <br/>`hot_wallet_min_balance:hot_wallet_max_balance:min_withdrawal_amount:hot_wallet_residual_balance`, <br/> example `1000000000:100000000000:1000000000:95000000000`                                                                                                                                                                         | \n| `COLD_WALLET`                | cold-wallet address, example `kQCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseH3XZ_YiH9Y1ufw`. If cold wallet is not active - use non-bounceable address (use https://ton.org/address for convert)                                                                                                                                                                                          |\n| `DEPOSIT_SIDE_BALANCE`       | `true` - service calculates total income for user by deposit incoming, `false` - by hot wallet incoming. Default: `true`.                                                                                                                                                                                                                                                     |\n| `QUEUE_ENABLED`              | `true` - service sends incoming notifications to queue, `false` - sending disabled. Default: `false`.                                                                                                                                                                                                                                                                         |\n| `QUEUE_URI`                  | URI for queue client connection, example `amqp://guest:guest@payment_rabbitmq:5672/`                                                                                                                                                                                                                                                                                          |\n| `QUEUE_NAME`                 | name of exchange                                                                                                                                                                                                                                                                                                                                                              |\n| `WEBHOOK_ENDPOINT`           | endpoint to send webhooks, example: `http://hostname:3333/webhook`. If the value is not set, then webhooks are not sent.                                                                                                                                                                                                                                                      |\n| `WEBHOOK_TOKEN`              | Bearer token for webhook request. If not set then not used.                                                                                                                                                                                                                                                                                                                   |\n| `ALLOWABLE_LAG`              | allowable time lag between service time and last block time in seconds, default: 15                                                                                                                                                                                                                                                                                           |\n| `FORWARD_TON_AMOUNT`         | forward ton amount for jetton withdrawals, default: 1 nanoton                                                                                                                                                                                                                                                                                                                 |\n| `PROOF_CHECK_ENABLED`        | enable verification of all proofs to securely connect to an untrusted node, default: `false`. Also you need to define `NETWORK_CONFIG_URL`.                                                                                                                                                                                                                                   |\n| `NETWORK_CONFIG_URL`         | the path to load the network configuration to get the trusted key block from it. This is necessary for proof verification, example: `https://ton.org/global.config.json`                                                                                                                                                                                                      |\n\n**! Be careful with `IS_TESTNET` variable.** This does not guarantee that a testnet node is being used. It is only for address checking purposes.\n\nThere are also internal service settings (fees and timeouts) that are specified in the source code in the [Config](/config/config.go) package.\nCalibration parameters recommendations in [Technical notes](/technical_notes.md).\n\n#### `hot_wallet_residual_balance` and `hot_wallet_max_balance`\n\nIn order to avoid triggering a withdrawal to a cold wallet with each receipt of funds, a hysteresis is introduced.\n`hot_wallet_max_balance` - this is the amount at which the withdrawal from the hot wallet to the cold one will be triggered\n`hot_wallet_residual_balance` is the amount that will remain on the hot wallet after the withdrawal\n\n`hot_wallet_max_balance` must be greater than `hot_wallet_residual_balance`\n\nIf the `hot_wallet_residual_balance` is not set, then it is calculated using the formula:\n`hot_wallet_residual_balance` = `hot_wallet_max_balance` * `hysteresis`, where hysteresis is a hardcoded value \n(at the time of writing this is 0.95)\n\n### Service deploy\n\n**Do not use same `.env` file for `payment-processor` and other services!**\n\n1. Build docker images from makefile \n```console\nmake -f Makefile\n```\n2. Prepare `.env` file for `payment-postgres` service or fill environment variables in `docker-compose.yml` file.\nDatabase scheme automatically init.\n```console\ndocker-compose -f docker-compose.yml up -d payment-postgres\n```\n3. Prepare `.env` file for `payment-processor` service or fill environment variables in `docker-compose.yml` file.\n```console\ndocker-compose -f docker-compose.yml up -d payment-processor\n```\n4. Optionally you can start Grafana for service monitoring. Prepare `.env` file for `payment-grafana` service or \nfill environment variables in `docker-compose.yml` file.\n```console\ndocker-compose -f docker-compose.yml up -d payment-grafana\n```\n5. Optionally you can start RabbitMQ to collect payment notifications (if `QUEUE_ENABLED` env var is `true` for payment-processor). \nPrepare `.env` file for `payment-rabbitmq` service or fill environment variables in `docker-compose.yml` file.\n```console\ndocker-compose -f docker-compose.yml up -d payment-rabbitmq\n```\n\n## Payment notifications\n\nATTENTION! Sending notifications does not guarantee that all notifications will be sent. \nIf the service is restarted after the data is saved to the database and before the notification data is sent, \nthese notifications will not be sent after restart.\n\nThe service has several mechanisms for notification of payments. These are webhooks and a AMQP (to RabbitMQ). \nDepending on the `DEPOSIT_SIDE_BALANCE` setting, a notification is received either about the payment to the \ndeposit address, or about the withdrawal from the deposit to the hot wallet. Source address and comment returned if known.\n\nMessage format when `DEPOSIT_SIDE_BALANCE` == true:\n```json\n{\n\t\"deposit_address\":\"0QCdsj-u39qVlfYdpPKuAY0hTe5VIsiJcpB5Rx4tOUOyBFhL\",\n\t\"time\": 12345678,\n\t\"amount\":\"100\", \n\t\"source_address\":\"0QAOp2OZwWdkF5HhJ0WVDspgh6HhpmHyQ3cBuBmfJ4q_AIVe\",\n\t\"comment\":\"hello\",\n    \"tx_hash\": \"f9b9e7efd3a38da318a894576499f0b6af5ca2da97ccd15c5f1d291a808a0ebf\",\n    \"user_id\": \"123\"\n}\n```\n\nMessage format when `DEPOSIT_SIDE_BALANCE` == false (there is fewer data, because the accumulated amount is withdrawn \nfrom the deposit):\n```json\n{\n\t\"deposit_address\":\"0QCdsj-u39qVlfYdpPKuAY0hTe5VIsiJcpB5Rx4tOUOyBFhL\",\n\t\"time\": 12345678,\n\t\"amount\":\"200\",\n    \"tx_hash\": \"f9b9e7efd3a38da318a894576499f0b6af5ca2da97ccd15c5f1d291a808a0ebf\",\n    \"user_id\": \"123\"\n}\n```\n\n### Using RabbitMQ\n1. Set `QUEUE_ENABLED = true` env variable\n2. Set `QUEUE_URI` as described at [Configurable parameters](#Configurable-parameters)\n3. Set `QUEUE_NAME` env variable. Be careful, this is not the name of a specific queue in the rabbit, but the name of \n   the exchange.\n4. Start RabbitMQ as described at [Service deploy](#Service-deploy)\n\n### Using webhooks\n1. Set `WEBHOOK_ENDPOINT` env variable\n2. Optionally set `WEBHOOK_TOKEN` env variable\n\nWhen the `payment-processor` is running, it will send a `POST` request to the webhook endpoint with each payment and \nwait for a response with a `200` code and an empty body. If a successful delivery response is not received after \nseveral attempts, the service will stop with an error. If the variable `WEBHOOK_TOKEN` is set, it will also \nadd header `Authorization: Bearer {token}`.\n\n## Binary comment support\n\nMethod `/v1/withdrawal/send` also supports `binary_comment`. The comment is written in a hex form. If the bits qty is not \na multiple of a byte, then the record form with a flip bit is supported, for example `9fe7_`.\nA `binary_comment` is writing directly to the body of the message (for the TON transfer) and to the `forward_payload` \n(for the Jetton transfer) with its opcode according to the following TLB scheme:\n\n`binary_comment#b3ddcf7d {n:#} data:(SnakeData ~n) = InternalMsgBody;`\n\n`crc32('binary_comment n:# data:SnakeData ~n = InternalMsgBody') = 0xb3ddcf7d`\n\nThis comment will not be displayed by the explorer as text and can be useful for transmitting metadata that \nwill be read by indexers.\n\nThe documentation [contains](https://docs.ton.org/develop/smart-contracts/guidelines/internal-messages#simple-message-with-comment) a standard way of writing a binary comment, but due to the fact that it is not supported \nby services, an alternative recording method was chosen.\n\n<!-- Badges -->\n[ton-svg]: https://img.shields.io/badge/Based%20on-TON-blue\n[ton]: https://ton.org\n[telegram-url]: https://t.me/tonbicycle\n[telegram-svg]: https://img.shields.io/badge/telegram-chat-blue?color=blue&logo=telegram&logoColor=white\n"
  },
  {
    "path": "api/example.http",
    "content": "###\nPOST {{url}}/v1/address/new\nAuthorization: Bearer {{token}}\nContent-Type: application/json\n\n{\"user_id\": \"TestUser\", \"currency\": \"TON\"}\n\n###\nPOST {{url}}/v1/address/new\nAuthorization: Bearer {{token}}\nContent-Type: application/json\n\n{\"user_id\": \"TestUser\", \"currency\": \"TGR\"}\n\n###\nGET {{url}}/v1/address/all?user_id=TestUser\nAuthorization: Bearer {{token}}\n\n###\nGET {{url}}/v1/income?user_id=TestUser\nAuthorization: Bearer {{token}}\n\n###\nGET {{url}}/v1/deposit/history?user_id=TestUser&currency=TON&limit=3&offset=0&sort_order=asc\nAuthorization: Bearer {{token}}\n\n###\nPOST {{url}}/v1/withdrawal/send\nAuthorization: Bearer {{token}}\nContent-Type: application/json\n\n{\"user_id\": \"TestUser\", \"query_id\": \"1\", \"currency\": \"TON\", \"amount\":  200000000, \"destination\": \"kQBFETbGASx3-6QYpPuQAKQM1s32AfSkWzbsADqt3bKDlN1A\", \"comment\":  \"test_ton_withdrawal\"}\n\n###\nPOST {{url}}/v1/withdrawal/send\nAuthorization: Bearer {{token}}\nContent-Type: application/json\n\n{\"user_id\": \"TestUser\", \"query_id\": \"2\", \"currency\": \"TGR\", \"amount\":  1000, \"destination\": \"kQBFETbGASx3-6QYpPuQAKQM1s32AfSkWzbsADqt3bKDlN1A\", \"comment\":  \"test_jetton_withdrawal\"}\n\n###\nPOST {{url}}/v1/withdrawal/send\nAuthorization: Bearer {{token}}\nContent-Type: application/json\n\n{\"user_id\": \"TestUser\", \"query_id\": \"3\", \"currency\": \"TON\", \"amount\":  1000, \"destination\": \"kQBFETbGASx3-6QYpPuQAKQM1s32AfSkWzbsADqt3bKDlN1A\", \"binary_comment\":  \"9fe7_\"}\n\n\n###\nPOST {{url}}/v1/withdrawal/service/jetton\nAuthorization: Bearer {{token}}\nContent-Type: application/json\n\n{\"owner\": \"0QCdsj-u39qVlfYdpPKuAY0hTe5VIsiJcpB5Rx4tOUOyBFhL\", \"jetton_master\": \"kQAbMQzuuGiCne0R7QEj9nrXsjM7gNjeVmrlBZouyC-SCALE\"}\n\n###\nPOST {{url}}/v1/withdrawal/service/ton\nAuthorization: Bearer {{token}}\nContent-Type: application/json\n\n{\"from\": \"0QAOp2OZwWdkF5HhJ0WVDspgh6HhpmHyQ3cBuBmfJ4q_AIVe\"}\n\n###\nGET {{url}}/v1/withdrawal/status?id=2\nAuthorization: Bearer {{token}}\n\n###\nGET {{url}}/v1/system/sync\n\n###\nGET {{url}}/metrics\n\n###\nGET {{url}}//v1/deposit/income?tx_hash=54e61136c33b94372030de8c7d02bc23a60e3de7cfad46f26258e8e722dc66b1\nAuthorization: Bearer {{token}}\n\n###\nGET {{url}}//v1/balance?currency=TON&address=kQAbMQzuuGiCne0R7QEj9nrXsjM7gNjeVmrlBZouyC-SCALE\nAuthorization: Bearer {{token}}\n\n###\nGET {{url}}//v1/balance?currency=TON\nAuthorization: Bearer {{token}}\n\n###\nGET {{url}}//v1/resolve?domain=wallet.ton\nAuthorization: Bearer {{token}}\n"
  },
  {
    "path": "api/handlers.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/gobicycle/bicycle/config\"\n\t\"github.com/gobicycle/bicycle/core\"\n\t\"github.com/gobicycle/bicycle/metrics\"\n\t\"github.com/gofrs/uuid\"\n\t\"github.com/shopspring/decimal\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/tonkeeper/tongo/boc\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"github.com/xssnick/tonutils-go/tlb\"\n\t\"github.com/xssnick/tonutils-go/ton/wallet\"\n\t\"math/big\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n)\n\ntype Handler struct {\n\tstorage          storage\n\tblockchain       blockchain\n\ttoken            string\n\tshard            byte\n\tmutex            sync.Mutex\n\thotWalletAddress address.Address\n}\n\ntype WithdrawalRequest struct {\n\tUserID        string     `json:\"user_id\"`\n\tQueryID       string     `json:\"query_id\"`\n\tCurrency      string     `json:\"currency\"`\n\tAmount        core.Coins `json:\"amount\"`\n\tDestination   string     `json:\"destination\"`\n\tComment       string     `json:\"comment\"`\n\tBinaryComment string     `json:\"binary_comment\"`\n}\n\ntype ServiceTonWithdrawalRequest struct {\n\tFrom string `json:\"from\"`\n}\n\ntype ServiceJettonWithdrawalRequest struct {\n\tOwner        string `json:\"owner\"`\n\tJettonMaster string `json:\"jetton_master\"`\n}\n\ntype WalletAddress struct {\n\tAddress  string `json:\"address\"`\n\tCurrency string `json:\"currency\"`\n}\n\ntype GetAddressesResponse struct {\n\tAddresses []WalletAddress `json:\"addresses\"`\n}\n\ntype WithdrawalResponse struct {\n\tID int64 `json:\"ID\"`\n}\n\ntype GetBalanceResponse struct {\n\tBalance          string `json:\"balance\"`\n\tStatus           string `json:\"status,omitempty\"`\n\tProcessingAmount string `json:\"total_processing_amount,omitempty\"`\n\tPendingAmount    string `json:\"total_pending_amount,omitempty\"`\n}\n\ntype ResolveResponse struct {\n\tAddress string `json:\"address\"`\n}\n\ntype WithdrawalStatusResponse struct {\n\tUserID  string                `json:\"user_id\"`\n\tQueryID string                `json:\"query_id\"`\n\tStatus  core.WithdrawalStatus `json:\"status\"`\n\tTxHash  string                `json:\"tx_hash,omitempty\"`\n}\n\ntype GetIncomeResponse struct {\n\tSide         string        `json:\"counting_side\"`\n\tTotalIncomes []totalIncome `json:\"total_income\"`\n}\n\ntype GetHistoryResponse struct {\n\tIncomes []income `json:\"incomes\"`\n}\n\ntype GetIncomeByTxResponse struct {\n\tCurrency string `json:\"currency\"`\n\tIncome   income `json:\"income\"`\n}\n\ntype totalIncome struct {\n\tAddress  string `json:\"deposit_address\"`\n\tAmount   string `json:\"amount\"`\n\tCurrency string `json:\"currency\"`\n}\n\ntype income struct {\n\tDepositAddress string `json:\"deposit_address\"`\n\tTime           int64  `json:\"time\"`\n\tSourceAddress  string `json:\"source_address,omitempty\"`\n\tAmount         string `json:\"amount\"`\n\tComment        string `json:\"comment,omitempty\"`\n\tTxHash         string `json:\"tx_hash,omitempty\"`\n}\n\nfunc NewHandler(s storage, b blockchain, token string, shard byte, hotWalletAddress address.Address) *Handler {\n\treturn &Handler{storage: s, blockchain: b, token: token, shard: shard, hotWalletAddress: hotWalletAddress}\n}\n\nfunc (h *Handler) getNewAddress(resp http.ResponseWriter, req *http.Request) {\n\tvar data struct {\n\t\tUserID   string `json:\"user_id\"`\n\t\tCurrency string `json:\"currency\"`\n\t}\n\terr := json.NewDecoder(req.Body).Decode(&data)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusBadRequest, fmt.Sprintf(\"decode payload data err: %v\", err))\n\t\treturn\n\t}\n\tif !isValidCurrency(data.Currency) {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"invalid currency type\")\n\t\treturn\n\t}\n\th.mutex.Lock()\n\tdefer h.mutex.Unlock() // To prevent data race\n\taddr, err := generateAddress(req.Context(), data.UserID, data.Currency, h.shard, h.storage, h.blockchain, h.hotWalletAddress)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"generate address err: %v\", err))\n\t\treturn\n\t}\n\tres := struct {\n\t\tAddress string `json:\"address\"`\n\t}{Address: addr}\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\terr = json.NewEncoder(resp).Encode(res)\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n}\n\nfunc (h *Handler) getAddresses(resp http.ResponseWriter, req *http.Request) {\n\tuserID := req.URL.Query().Get(\"user_id\")\n\tif userID == \"\" {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"need to provide user ID\")\n\t\treturn\n\t}\n\taddresses, err := getAddresses(req.Context(), userID, h.storage)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"get addresses err: %v\", err))\n\t\treturn\n\t}\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\terr = json.NewEncoder(resp).Encode(addresses)\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n}\n\nfunc (h *Handler) sendWithdrawal(resp http.ResponseWriter, req *http.Request) {\n\tvar body WithdrawalRequest\n\terr := json.NewDecoder(req.Body).Decode(&body)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusBadRequest, fmt.Sprintf(\"decode payload err: %v\", err))\n\t\treturn\n\t}\n\tw, err := convertWithdrawal(body)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusBadRequest, fmt.Sprintf(\"convert withdrawal err: %v\", err))\n\t\treturn\n\t}\n\tunique, err := h.storage.IsWithdrawalRequestUnique(req.Context(), w)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"check withdrawal uniquess err: %v\", err))\n\t\treturn\n\t} else if !unique {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"(user_id,query_id) not unique\")\n\t\treturn\n\t}\n\t_, ok := h.storage.GetWalletType(w.Destination)\n\tif ok {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"withdrawal to service internal addresses not supported\")\n\t\treturn\n\t}\n\tid, err := h.storage.SaveWithdrawalRequest(req.Context(), w)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"save withdrawal request err: %v\", err))\n\t\treturn\n\t}\n\tr := WithdrawalResponse{ID: id}\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\terr = json.NewEncoder(resp).Encode(r)\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n}\n\nfunc (h *Handler) getSync(resp http.ResponseWriter, req *http.Request) {\n\n\tisSynced, utime, err := h.storage.IsActualBlockData(req.Context())\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"get sync from db err: %v\", err))\n\t\treturn\n\t}\n\tgetSyncResponse := struct {\n\t\tIsSynced  bool  `json:\"is_synced\"`\n\t\tBlockTime int64 `json:\"last_block_gen_utime\"`\n\t}{\n\t\tIsSynced:  isSynced,\n\t\tBlockTime: utime,\n\t}\n\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\terr = json.NewEncoder(resp).Encode(getSyncResponse)\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n}\n\nfunc (h *Handler) getWithdrawalStatus(resp http.ResponseWriter, req *http.Request) {\n\tids := req.URL.Query().Get(\"id\")\n\tif ids == \"\" {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"need to provide request ID\")\n\t\treturn\n\t}\n\tid, err := strconv.ParseInt(ids, 10, 64)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusBadRequest, fmt.Sprintf(\"convert request ID err: %v\", err))\n\t\treturn\n\t}\n\tstatus, err := h.storage.GetExternalWithdrawalStatus(req.Context(), id)\n\tif errors.Is(err, core.ErrNotFound) {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"request ID not found\")\n\t\treturn\n\t}\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"get external withdrawal status err: %v\", err))\n\t\treturn\n\t}\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\n\tres := WithdrawalStatusResponse{\n\t\tUserID:  status.UserID,\n\t\tQueryID: status.QueryID,\n\t\tStatus:  status.Status,\n\t}\n\n\tif status.TxHash != nil {\n\t\tres.TxHash = fmt.Sprintf(\"%x\", status.TxHash)\n\t}\n\n\terr = json.NewEncoder(resp).Encode(res)\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n}\n\nfunc (h *Handler) getIncome(resp http.ResponseWriter, req *http.Request) {\n\tid := req.URL.Query().Get(\"user_id\")\n\tif id == \"\" {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"need to provide user ID\")\n\t\treturn\n\t}\n\ttotalIncomes, err := h.storage.GetIncome(req.Context(), id, config.Config.IsDepositSideCalculation)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"get income err: %v\", err))\n\t\treturn\n\t}\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\terr = json.NewEncoder(resp).Encode(convertIncome(h.storage, totalIncomes))\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n}\n\nfunc (h *Handler) getIncomeHistory(resp http.ResponseWriter, req *http.Request) {\n\tid := req.URL.Query().Get(\"user_id\")\n\tif id == \"\" {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"need to provide user ID\")\n\t\treturn\n\t}\n\tcurrency := req.URL.Query().Get(\"currency\")\n\tif currency == \"\" {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"need to provide currency\")\n\t\treturn\n\t}\n\tif !isValidCurrency(currency) {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"invalid currency type\")\n\t\treturn\n\t}\n\tlimit, err := strconv.Atoi(req.URL.Query().Get(\"limit\"))\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"invalid limit parameter\")\n\t\treturn\n\t}\n\toffset, err := strconv.Atoi(req.URL.Query().Get(\"offset\"))\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"invalid offset parameter\")\n\t\treturn\n\t}\n\n\tascOrder := false\n\tsort := strings.ToLower(req.URL.Query().Get(\"sort_order\"))\n\tif sort == \"asc\" {\n\t\tascOrder = true\n\t} else if sort != \"\" && sort != \"desc\" {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"invalid sort order\")\n\t\treturn\n\t}\n\n\thistory, err := h.storage.GetIncomeHistory(req.Context(), id, currency, limit, offset, ascOrder)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"get history err: %v\", err))\n\t\treturn\n\t}\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\terr = json.NewEncoder(resp).Encode(convertHistory(h.storage, currency, history))\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n}\n\nfunc (h *Handler) serviceTonWithdrawal(resp http.ResponseWriter, req *http.Request) {\n\tvar body ServiceTonWithdrawalRequest\n\terr := json.NewDecoder(req.Body).Decode(&body)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusBadRequest, fmt.Sprintf(\"decode payload err: %v\", err))\n\t\treturn\n\t}\n\tw, err := convertTonServiceWithdrawal(h.storage, body)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusBadRequest, fmt.Sprintf(\"convert service withdrawal err: %v\", err))\n\t\treturn\n\t}\n\tmemo, err := h.storage.SaveServiceWithdrawalRequest(req.Context(), w)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"save service withdrawal request err: %v\", err))\n\t\treturn\n\t}\n\tvar response = struct {\n\t\tMemo uuid.UUID `json:\"memo\"`\n\t}{\n\t\tMemo: memo,\n\t}\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\terr = json.NewEncoder(resp).Encode(response)\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n}\n\nfunc (h *Handler) serviceJettonWithdrawal(resp http.ResponseWriter, req *http.Request) {\n\tvar body ServiceJettonWithdrawalRequest\n\terr := json.NewDecoder(req.Body).Decode(&body)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusBadRequest, fmt.Sprintf(\"decode payload err: %v\", err))\n\t\treturn\n\t}\n\tw, err := convertJettonServiceWithdrawal(h.storage, body)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusBadRequest, fmt.Sprintf(\"convert service withdrawal err: %v\", err))\n\t\treturn\n\t}\n\tmemo, err := h.storage.SaveServiceWithdrawalRequest(req.Context(), w)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"save service withdrawal request err: %v\", err))\n\t\treturn\n\t}\n\tvar response = struct {\n\t\tMemo uuid.UUID `json:\"memo\"`\n\t}{\n\t\tMemo: memo,\n\t}\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\terr = json.NewEncoder(resp).Encode(response)\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n}\n\nfunc (h *Handler) getMetrics(resp http.ResponseWriter, req *http.Request) {\n\tbuf := new(bytes.Buffer)\n\tfor _, m := range metrics.AllMetrics {\n\t\terr := m.Print(buf)\n\t\tif err != nil {\n\t\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"get metrics err: %v\", err.Error()))\n\t\t\treturn\n\t\t}\n\t}\n\tresp.Header().Add(\"Content-Type\", \"application/text\")\n\tresp.WriteHeader(http.StatusOK)\n\t_, err := resp.Write(buf.Bytes())\n\tif err != nil {\n\t\tlog.Errorf(\"buffer writing error: %v\", err)\n\t}\n}\n\nfunc (h *Handler) getIncomeByTx(resp http.ResponseWriter, req *http.Request) {\n\ttxHash := strings.ToLower(req.URL.Query().Get(\"tx_hash\"))\n\thash, err := hex.DecodeString(txHash)\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusBadRequest, fmt.Sprintf(\"get tx hash err: %v\", err.Error()))\n\t\treturn\n\t}\n\tif len(hash) != 32 {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"invalid hash len\")\n\t\treturn\n\t}\n\toneIncome, currency, err := h.storage.GetIncomeByTx(req.Context(), hash)\n\tif errors.Is(err, core.ErrNotFound) {\n\t\twriteHttpError(resp, http.StatusNotFound, \"transaction not found\")\n\t\treturn\n\t}\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"get income by tx err: %v\", err))\n\t\treturn\n\t}\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\tres := GetIncomeByTxResponse{Currency: currency, Income: convertOneIncome(h.storage, currency, *oneIncome)}\n\terr = json.NewEncoder(resp).Encode(res)\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n}\n\nfunc (h *Handler) getBalance(resp http.ResponseWriter, req *http.Request) {\n\n\tcurrency := req.URL.Query().Get(\"currency\")\n\tif currency == \"\" {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"need to provide currency\")\n\t\treturn\n\t}\n\tif !isValidCurrency(currency) {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"invalid currency type\")\n\t\treturn\n\t}\n\n\tvar (\n\t\ttonWalletAddress core.Address\n\t\terr              error\n\t\tbalance          *big.Int\n\t\tstatus           tlb.AccountStatus\n\t\tres              GetBalanceResponse\n\t)\n\n\taddr := req.URL.Query().Get(\"address\")\n\tif addr != \"\" {\n\t\ttonWalletAddress, _, err = validateAddress(addr)\n\t\tif err != nil {\n\t\t\twriteHttpError(resp, http.StatusBadRequest, fmt.Sprintf(\"invalid address: %s\", err.Error()))\n\t\t\treturn\n\t\t}\n\t} else {\n\t\ttonWalletAddress = core.AddressMustFromTonutilsAddress(&h.hotWalletAddress)\n\t\tamounts, err := h.storage.GetTotalWithdrawalAmounts(req.Context(), currency)\n\t\tif err != nil {\n\t\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"get total withdrawal amounts err: %v\", err))\n\t\t\treturn\n\t\t}\n\t\tres.PendingAmount = amounts.Pending.String()\n\t\tres.ProcessingAmount = amounts.Processing.String()\n\t}\n\n\tif currency == core.TonSymbol {\n\n\t\tbalance, status, err = h.blockchain.GetAccountCurrentState(req.Context(), tonWalletAddress.ToTonutilsAddressStd(0))\n\t\tif err != nil {\n\t\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"get TON balance err: %v\", err))\n\t\t\treturn\n\t\t}\n\t\tres.Status = strings.ToLower(string(status))\n\n\t} else {\n\n\t\tjetton, _ := config.Config.Jettons[currency] // currency validate earlier\n\t\tbalance, err = h.blockchain.GetJettonBalanceByOwner(req.Context(), tonWalletAddress.ToTonutilsAddressStd(0), jetton.Master)\n\t\tif err != nil {\n\t\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"get jetton balance err: %v\", err))\n\t\t\treturn\n\t\t}\n\n\t}\n\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\tres.Balance = balance.String()\n\terr = json.NewEncoder(resp).Encode(res)\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n\n}\n\nfunc (h *Handler) getResolve(resp http.ResponseWriter, req *http.Request) {\n\n\tdomain := req.URL.Query().Get(\"domain\")\n\tif domain == \"\" {\n\t\twriteHttpError(resp, http.StatusBadRequest, \"invalid domain\")\n\t\treturn\n\t}\n\n\taddr, err := h.blockchain.DnsResolveSmc(req.Context(), domain)\n\tif errors.Is(err, core.ErrNotFound) {\n\t\twriteHttpError(resp, http.StatusNotFound, \"smart contract DNS record not found\")\n\t\treturn\n\t}\n\tif err != nil {\n\t\twriteHttpError(resp, http.StatusInternalServerError, fmt.Sprintf(\"get DNS record err: %v\", err))\n\t\treturn\n\t}\n\n\taddr.SetTestnetOnly(config.Config.Testnet)\n\taddr.SetBounce(true)\n\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(http.StatusOK)\n\tres := ResolveResponse{Address: addr.String()}\n\terr = json.NewEncoder(resp).Encode(res)\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n\n}\n\nfunc RegisterHandlers(mux *http.ServeMux, h *Handler) {\n\tmux.HandleFunc(\"/v1/address/new\", recoverMiddleware(authMiddleware(post(h.getNewAddress))))\n\tmux.HandleFunc(\"/v1/address/all\", recoverMiddleware(authMiddleware(get(h.getAddresses))))\n\tmux.HandleFunc(\"/v1/withdrawal/send\", recoverMiddleware(authMiddleware(post(h.sendWithdrawal))))\n\tmux.HandleFunc(\"/v1/withdrawal/service/ton\", recoverMiddleware(authMiddleware(post(h.serviceTonWithdrawal))))\n\tmux.HandleFunc(\"/v1/withdrawal/service/jetton\", recoverMiddleware(authMiddleware(post(h.serviceJettonWithdrawal))))\n\tmux.HandleFunc(\"/v1/withdrawal/status\", recoverMiddleware(authMiddleware(get(h.getWithdrawalStatus))))\n\tmux.HandleFunc(\"/v1/system/sync\", recoverMiddleware(get(h.getSync)))\n\tmux.HandleFunc(\"/v1/income\", recoverMiddleware(authMiddleware(get(h.getIncome))))\n\tmux.HandleFunc(\"/v1/deposit/history\", recoverMiddleware(authMiddleware(get(h.getIncomeHistory))))\n\tmux.HandleFunc(\"/v1/deposit/income\", recoverMiddleware(authMiddleware(get(h.getIncomeByTx))))\n\tmux.HandleFunc(\"/v1/balance\", recoverMiddleware(authMiddleware(get(h.getBalance))))\n\tmux.HandleFunc(\"/v1/resolve\", recoverMiddleware(authMiddleware(get(h.getResolve))))\n\tmux.HandleFunc(\"/metrics\", recoverMiddleware(get(h.getMetrics)))\n}\n\nfunc generateAddress(\n\tctx context.Context,\n\tuserID string,\n\tcurrency string,\n\tshard byte,\n\tdbConn storage,\n\tbc blockchain,\n\thotWalletAddress address.Address,\n) (\n\tstring,\n\terror,\n) {\n\tsubwalletID, err := dbConn.GetLastSubwalletID(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar res string\n\tif currency == core.TonSymbol {\n\t\tw, id, err := bc.GenerateSubWallet(config.Config.Seed, shard, subwalletID+1)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\ta, err := core.AddressFromTonutilsAddress(w.Address())\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\terr = dbConn.SaveTonWallet(ctx,\n\t\t\tcore.WalletData{\n\t\t\t\tSubwalletID: id,\n\t\t\t\tUserID:      userID,\n\t\t\t\tCurrency:    core.TonSymbol,\n\t\t\t\tType:        core.TonDepositWallet,\n\t\t\t\tAddress:     a,\n\t\t\t},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tres = a.ToUserFormat()\n\t} else {\n\t\tjetton, ok := config.Config.Jettons[currency]\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"jetton address not found\")\n\t\t}\n\t\tproxy, addr, err := bc.GenerateDepositJettonWalletForProxy(ctx, shard, &hotWalletAddress, jetton.Master, subwalletID+1)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tjettonWalletAddr, err := core.AddressFromTonutilsAddress(addr)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tproxyAddr, err := core.AddressFromTonutilsAddress(proxy.Address())\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\terr = dbConn.SaveJettonWallet(\n\t\t\tctx,\n\t\t\tproxyAddr,\n\t\t\tcore.WalletData{\n\t\t\t\tUserID:      userID,\n\t\t\t\tSubwalletID: proxy.SubwalletID,\n\t\t\t\tCurrency:    currency,\n\t\t\t\tType:        core.JettonDepositWallet,\n\t\t\t\tAddress:     jettonWalletAddr,\n\t\t\t},\n\t\t\tfalse,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tres = proxyAddr.ToUserFormat()\n\t}\n\treturn res, nil\n}\n\nfunc getAddresses(ctx context.Context, userID string, dbConn storage) (GetAddressesResponse, error) {\n\tvar res = GetAddressesResponse{\n\t\tAddresses: []WalletAddress{},\n\t}\n\ttonAddr, err := dbConn.GetTonWalletsAddresses(ctx, userID, []core.WalletType{core.TonDepositWallet})\n\tif err != nil {\n\t\treturn GetAddressesResponse{}, err\n\t}\n\tjettonAddr, err := dbConn.GetJettonOwnersAddresses(ctx, userID, []core.WalletType{core.JettonDepositWallet})\n\tif err != nil {\n\t\treturn GetAddressesResponse{}, err\n\t}\n\tfor _, a := range tonAddr {\n\t\tres.Addresses = append(res.Addresses, WalletAddress{Address: a.ToUserFormat(), Currency: core.TonSymbol})\n\t}\n\tfor _, a := range jettonAddr {\n\t\tres.Addresses = append(res.Addresses, WalletAddress{Address: a.Address.ToUserFormat(), Currency: a.Currency})\n\t}\n\treturn res, nil\n}\n\nfunc isValidCommentLen(comment string) bool {\n\treturn len(comment) < config.MaxCommentLength\n}\n\nfunc isValidCurrency(cur string) bool {\n\tif _, ok := config.Config.Jettons[cur]; ok || cur == core.TonSymbol {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc convertWithdrawal(w WithdrawalRequest) (core.WithdrawalRequest, error) {\n\n\tif !isValidCurrency(w.Currency) {\n\t\treturn core.WithdrawalRequest{}, fmt.Errorf(\"invalid currency\")\n\t}\n\n\taddr, bounceable, err := validateAddress(w.Destination)\n\tif err != nil {\n\t\treturn core.WithdrawalRequest{}, fmt.Errorf(\"invalid destination address: %v\", err)\n\t}\n\n\tif !(w.Amount.Cmp(decimal.New(0, 0)) == 1) {\n\t\treturn core.WithdrawalRequest{}, fmt.Errorf(\"amount must be > 0\")\n\t}\n\n\tif w.Comment != \"\" && w.BinaryComment != \"\" {\n\t\treturn core.WithdrawalRequest{}, fmt.Errorf(\"only one type of comment can be specified (comment OR binary comment)\")\n\t}\n\n\tif !isValidCommentLen(w.Comment) || !isValidCommentLen(w.BinaryComment) {\n\t\treturn core.WithdrawalRequest{}, fmt.Errorf(\"too long comment, max length allowed: %d\", config.MaxCommentLength)\n\t}\n\n\tres := core.WithdrawalRequest{\n\t\tUserID:      w.UserID,\n\t\tQueryID:     w.QueryID,\n\t\tCurrency:    w.Currency,\n\t\tAmount:      w.Amount,\n\t\tDestination: addr,\n\t\tBounceable:  bounceable,\n\t\tComment:     w.Comment,\n\t\tIsInternal:  false,\n\t}\n\n\tif w.BinaryComment != \"\" {\n\t\t_, err = boc.BitStringFromFiftHex(w.BinaryComment)\n\t\tif err != nil {\n\t\t\treturn core.WithdrawalRequest{}, fmt.Errorf(\"decode binary comment error: %v\", err)\n\t\t}\n\t\tres.BinaryComment = w.BinaryComment\n\t}\n\n\treturn res, nil\n}\n\nfunc convertTonServiceWithdrawal(s storage, w ServiceTonWithdrawalRequest) (core.ServiceWithdrawalRequest, error) {\n\tfrom, _, err := validateAddress(w.From)\n\tif err != nil {\n\t\treturn core.ServiceWithdrawalRequest{}, fmt.Errorf(\"invalid from address: %v\", err)\n\t}\n\tt, ok := s.GetWalletType(from)\n\tif !ok {\n\t\treturn core.ServiceWithdrawalRequest{}, fmt.Errorf(\"unknown deposit address\")\n\t}\n\tif t != core.JettonOwner {\n\t\treturn core.ServiceWithdrawalRequest{},\n\t\t\tfmt.Errorf(\"service withdrawal allowed only for Jetton deposit owner\")\n\t}\n\treturn core.ServiceWithdrawalRequest{\n\t\tFrom: from,\n\t}, nil\n}\n\nfunc convertJettonServiceWithdrawal(s storage, w ServiceJettonWithdrawalRequest) (core.ServiceWithdrawalRequest, error) {\n\tfrom, _, err := validateAddress(w.Owner)\n\tif err != nil {\n\t\treturn core.ServiceWithdrawalRequest{}, fmt.Errorf(\"invalid from address: %v\", err)\n\t}\n\tt, ok := s.GetWalletType(from)\n\tif !ok {\n\t\treturn core.ServiceWithdrawalRequest{}, fmt.Errorf(\"unknown deposit address\")\n\t}\n\tif t != core.JettonOwner && t != core.TonDepositWallet {\n\t\treturn core.ServiceWithdrawalRequest{},\n\t\t\tfmt.Errorf(\"service withdrawal allowed only for Jetton deposit owner or TON deposit\")\n\t}\n\tjetton, _, err := validateAddress(w.JettonMaster)\n\tif err != nil {\n\t\treturn core.ServiceWithdrawalRequest{}, fmt.Errorf(\"invalid jetton master address: %v\", err)\n\t}\n\t// currency type checks by withdrawal processor\n\treturn core.ServiceWithdrawalRequest{\n\t\tFrom:         from,\n\t\tJettonMaster: &jetton,\n\t}, nil\n}\n\nfunc convertIncome(dbConn storage, totalIncomes []core.TotalIncome) GetIncomeResponse {\n\tvar res = GetIncomeResponse{\n\t\tTotalIncomes: []totalIncome{},\n\t}\n\tif config.Config.IsDepositSideCalculation {\n\t\tres.Side = core.SideDeposit\n\t} else {\n\t\tres.Side = core.SideHotWallet\n\t}\n\n\tfor _, b := range totalIncomes {\n\t\ttotIncome := totalIncome{\n\t\t\tAmount:   b.Amount.String(),\n\t\t\tCurrency: b.Currency,\n\t\t}\n\t\tif b.Currency == core.TonSymbol {\n\t\t\ttotIncome.Address = b.Deposit.ToUserFormat()\n\t\t} else {\n\t\t\towner := dbConn.GetOwner(b.Deposit)\n\t\t\tif owner == nil {\n\t\t\t\t// TODO: remove fatal\n\t\t\t\tlog.Fatalf(\"can not find owner for deposit: %s\", b.Deposit.ToUserFormat())\n\t\t\t}\n\t\t\ttotIncome.Address = owner.ToUserFormat()\n\t\t}\n\t\tres.TotalIncomes = append(res.TotalIncomes, totIncome)\n\t}\n\treturn res\n}\n\nfunc convertOneIncome(dbConn storage, currency string, oneIncome core.ExternalIncome) income {\n\tinc := income{\n\t\tTime:    int64(oneIncome.Utime),\n\t\tAmount:  oneIncome.Amount.String(),\n\t\tComment: oneIncome.Comment,\n\t\tTxHash:  fmt.Sprintf(\"%x\", oneIncome.TxHash),\n\t}\n\tif currency == core.TonSymbol {\n\t\tinc.DepositAddress = oneIncome.To.ToUserFormat()\n\t} else {\n\t\towner := dbConn.GetOwner(oneIncome.To)\n\t\tif owner == nil {\n\t\t\t// TODO: remove fatal\n\t\t\tlog.Fatalf(\"can not find owner for deposit: %s\", oneIncome.To.ToUserFormat())\n\t\t}\n\t\tinc.DepositAddress = owner.ToUserFormat()\n\t}\n\t// show only std address\n\tif len(oneIncome.From) == 32 && oneIncome.FromWorkchain != nil {\n\t\taddr := address.NewAddress(0, byte(*oneIncome.FromWorkchain), oneIncome.From)\n\t\taddr.SetTestnetOnly(config.Config.Testnet)\n\t\tinc.SourceAddress = addr.String()\n\t}\n\treturn inc\n}\n\nfunc convertHistory(dbConn storage, currency string, incomes []core.ExternalIncome) GetHistoryResponse {\n\tvar res = GetHistoryResponse{\n\t\tIncomes: []income{},\n\t}\n\tfor _, i := range incomes {\n\t\tinc := convertOneIncome(dbConn, currency, i)\n\t\tres.Incomes = append(res.Incomes, inc)\n\t}\n\treturn res\n}\n\nfunc validateAddress(addr string) (core.Address, bool, error) {\n\tif addr == \"\" {\n\t\treturn core.Address{}, false, fmt.Errorf(\"empty address\")\n\t}\n\ta, err := address.ParseAddr(addr)\n\tif err != nil {\n\t\treturn core.Address{}, false, fmt.Errorf(\"invalid address: %v\", err)\n\t}\n\tif a.IsTestnetOnly() && !config.Config.Testnet {\n\t\treturn core.Address{}, false, fmt.Errorf(\"address for testnet only\")\n\t}\n\tif a.Workchain() != core.DefaultWorkchain {\n\t\treturn core.Address{}, false, fmt.Errorf(\"address must be in %d workchain\",\n\t\t\tcore.DefaultWorkchain)\n\t}\n\tres, err := core.AddressFromTonutilsAddress(a)\n\treturn res, a.IsBounceable(), err\n}\n\ntype storage interface {\n\tGetLastSubwalletID(ctx context.Context) (uint32, error)\n\tSaveTonWallet(ctx context.Context, walletData core.WalletData) error\n\tSaveJettonWallet(ctx context.Context, ownerAddress core.Address, walletData core.WalletData, notSaveOwner bool) error\n\tGetTonWalletsAddresses(ctx context.Context, userID string, types []core.WalletType) ([]core.Address, error)\n\tGetJettonOwnersAddresses(ctx context.Context, userID string, types []core.WalletType) ([]core.OwnerWallet, error)\n\tSaveWithdrawalRequest(ctx context.Context, w core.WithdrawalRequest) (int64, error)\n\tIsWithdrawalRequestUnique(ctx context.Context, w core.WithdrawalRequest) (bool, error)\n\tIsActualBlockData(ctx context.Context) (bool, int64, error)\n\tGetExternalWithdrawalStatus(ctx context.Context, id int64) (core.WithdrawalData, error)\n\tGetWalletType(address core.Address) (core.WalletType, bool)\n\tGetIncome(ctx context.Context, userID string, isDepositSide bool) ([]core.TotalIncome, error)\n\tSaveServiceWithdrawalRequest(ctx context.Context, w core.ServiceWithdrawalRequest) (uuid.UUID, error)\n\tGetIncomeHistory(ctx context.Context, userID string, currency string, limit int, offset int, ascOrder bool) ([]core.ExternalIncome, error)\n\tGetOwner(address core.Address) *core.Address\n\tGetIncomeByTx(ctx context.Context, txHash []byte) (*core.ExternalIncome, string, error)\n\tGetTotalWithdrawalAmounts(ctx context.Context, currency string) (*core.TotalWithdrawalsAmount, error)\n}\n\ntype blockchain interface {\n\tGenerateSubWallet(seed string, shard byte, startSubWalletID uint32) (*wallet.Wallet, uint32, error)\n\tGenerateDepositJettonWalletForProxy(\n\t\tctx context.Context,\n\t\tshard byte,\n\t\tproxyOwner, jettonMaster *address.Address,\n\t\tstartSubWalletID uint32,\n\t) (\n\t\tproxy *core.JettonProxy,\n\t\taddr *address.Address,\n\t\terr error,\n\t)\n\tGenerateDefaultWallet(seed string, isHighload bool) (*wallet.Wallet, byte, uint32, error)\n\tGetAccountCurrentState(ctx context.Context, address *address.Address) (*big.Int, tlb.AccountStatus, error)\n\tGetJettonBalanceByOwner(ctx context.Context, owner *address.Address, jettonMaster *address.Address) (*big.Int, error)\n\tDnsResolveSmc(ctx context.Context, domainName string) (*address.Address, error)\n}\n"
  },
  {
    "path": "api/http-client.env.json",
    "content": "{\n  \"local\": {\n    \"url\": \"http://localhost:8081\"\n  }\n}"
  },
  {
    "path": "api/middleware.go",
    "content": "package api\n\nimport (\n\t\"crypto/subtle\"\n\t\"encoding/json\"\n\t\"github.com/gobicycle/bicycle/config\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"net/http\"\n\t\"runtime/debug\"\n\t\"strings\"\n)\n\nfunc recoverMiddleware(next func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\tlog.Errorf(\n\t\t\t\t\t\"err: %v trace %v\", err, debug.Stack(),\n\t\t\t\t)\n\t\t\t}\n\t\t}()\n\t\tnext(w, r)\n\t}\n}\n\nfunc authMiddleware(next func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tif !checkToken(r, config.Config.APIToken) {\n\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\tnext(w, r)\n\t}\n}\n\nfunc get(next func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != http.MethodGet {\n\t\t\twriteHttpError(w, http.StatusBadRequest, \"only GET method is supported\")\n\t\t\treturn\n\t\t}\n\t\tnext(w, r)\n\t}\n}\n\nfunc post(next func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != http.MethodPost {\n\t\t\twriteHttpError(w, http.StatusBadRequest, \"only POST method is supported\")\n\t\t\treturn\n\t\t}\n\t\tnext(w, r)\n\t}\n}\n\nfunc checkToken(req *http.Request, token string) bool {\n\tauth := strings.Split(req.Header.Get(\"authorization\"), \" \")\n\tif len(auth) != 2 {\n\t\treturn false\n\t}\n\tif auth[0] != \"Bearer\" {\n\t\treturn false\n\t}\n\tif x := subtle.ConstantTimeCompare([]byte(auth[1]), []byte(token)); x == 1 {\n\t\treturn true\n\t} // constant time comparison to prevent time attack\n\treturn false\n}\n\nfunc writeHttpError(resp http.ResponseWriter, status int, comment string) {\n\tbody := struct {\n\t\tError string `json:\"error\"`\n\t}{\n\t\tError: comment,\n\t}\n\tresp.Header().Add(\"Content-Type\", \"application/json\")\n\tresp.WriteHeader(status)\n\terr := json.NewEncoder(resp).Encode(body)\n\tif err != nil {\n\t\tlog.Errorf(\"json encode error: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "audit/log.go",
    "content": "package audit\n\nimport (\n\t\"fmt\"\n\t\"github.com/gobicycle/bicycle/metrics\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"time\"\n)\n\ntype Severity string\n\nconst (\n\tError   Severity = \"ERROR\"\n\tWarning Severity = \"WARNING\"\n\tInfo    Severity = \"INFO\"\n)\n\ntype message struct {\n\tSeverity Severity\n\tText     string\n}\n\nfunc pushLog(m message) {\n\tswitch m.Severity {\n\tcase Error:\n\t\tlog.Printf(\"AUDIT|%v|%v|%s\", m.Severity, time.Now().Format(time.RFC1123), m.Text)\n\t\tmetrics.Errors.Inc()\n\tcase Warning:\n\t\tlog.Printf(\"AUDIT|%v|%v|%s\", m.Severity, time.Now().Format(time.RFC1123), m.Text)\n\t\tmetrics.Warnings.Inc()\n\tcase Info:\n\t\tlog.Printf(\"AUDIT|%v|%v|%s\", m.Severity, time.Now().Format(time.RFC1123), m.Text)\n\t\tmetrics.Info.Inc()\n\t}\n}\n\nfunc LogTX(severity Severity, location string, hash []byte, text string) {\n\tpushLog(message{\n\t\tSeverity: severity,\n\t\tText:     fmt.Sprintf(\"%s|TX:%x|%s\", location, hash, text),\n\t})\n}\n\nfunc Log(severity Severity, location, event, text string) {\n\tpushLog(message{\n\t\tSeverity: severity,\n\t\tText:     fmt.Sprintf(\"%s|%s|%s\", location, event, text),\n\t})\n}\n"
  },
  {
    "path": "blockchain/blockchain.go",
    "content": "package blockchain\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/gobicycle/bicycle/config\"\n\t\"github.com/gobicycle/bicycle/core\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/tonkeeper/tongo\"\n\t\"github.com/tonkeeper/tongo/boc\"\n\ttongoTlb \"github.com/tonkeeper/tongo/tlb\"\n\t\"github.com/tonkeeper/tongo/tvm\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"github.com/xssnick/tonutils-go/liteclient\"\n\t\"github.com/xssnick/tonutils-go/tl\"\n\t\"github.com/xssnick/tonutils-go/tlb\"\n\t\"github.com/xssnick/tonutils-go/ton\"\n\t\"github.com/xssnick/tonutils-go/ton/dns\"\n\t\"github.com/xssnick/tonutils-go/ton/jetton\"\n\t\"github.com/xssnick/tonutils-go/ton/wallet\"\n\t\"github.com/xssnick/tonutils-go/tvm/cell\"\n\t\"math\"\n\t\"math/big\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype Connection struct {\n\tclient   ton.APIClientWrapped\n\tresolver *dns.Client\n}\n\nfunc (c *Connection) WaitForBlock(seqno uint32) ton.APIClientWrapped {\n\treturn c.client.WaitForBlock(seqno)\n}\n\nfunc (c *Connection) FindLastTransactionByInMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (c *Connection) FindLastTransactionByOutMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\ntype contract struct {\n\tAddress tongo.AccountID\n\tCode    *boc.Cell\n\tData    *boc.Cell\n}\n\n// NewConnection creates new Blockchain connection\nfunc NewConnection(addr, key string, rateLimit int) (*Connection, error) {\n\n\tclient := liteclient.NewConnectionPool()\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*120)\n\tdefer cancel()\n\n\terr := client.AddConnection(ctx, addr, key)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"connection err: %v\", err.Error())\n\t}\n\n\tlimitedClient := newLimitedClient(client, rateLimit)\n\n\tvar wrappedClient ton.APIClientWrapped\n\n\tif config.Config.ProofCheckEnabled {\n\n\t\tif config.Config.NetworkConfigUrl == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"empty network config URL\")\n\t\t}\n\n\t\tcfg, err := liteclient.GetConfigFromUrl(ctx, config.Config.NetworkConfigUrl)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"get network config from url err: %s\", err.Error())\n\t\t}\n\n\t\twrappedClient = ton.NewAPIClient(limitedClient, ton.ProofCheckPolicySecure).WithRetry()\n\t\twrappedClient.SetTrustedBlockFromConfig(cfg)\n\n\t\tlog.Infof(\"Fetching and checking proofs since config init block ...\")\n\t\t_, err = wrappedClient.CurrentMasterchainInfo(ctx) // we fetch block just to trigger chain proof check\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"get masterchain info err: %s\", err.Error())\n\t\t}\n\t\tlog.Infof(\"Proof checks are completed\")\n\n\t} else {\n\t\twrappedClient = ton.NewAPIClient(limitedClient, ton.ProofCheckPolicyUnsafe).WithRetry()\n\t}\n\n\t// TODO: replace after tonutils fix\n\trootDNS, bcConfig, err := getConfigData(ctx, wrappedClient)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get config data err: %s\", err.Error())\n\t}\n\n\tconfig.Config.BlockchainConfig = bcConfig\n\tresolver := dns.NewDNSClient(wrappedClient, rootDNS)\n\n\treturn &Connection{\n\t\tclient:   wrappedClient,\n\t\tresolver: resolver,\n\t}, nil\n}\n\nfunc getConfigData(ctx context.Context, api ton.APIClientWrapped) (*address.Address, *boc.Cell, error) {\n\n\tb, err := api.CurrentMasterchainInfo(ctx)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to get masterchain info: %w\", err)\n\t}\n\n\tcfg, err := api.GetBlockchainConfig(ctx, b, 4)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to get blockchain config: %w\", err)\n\t}\n\n\tcfgDict, err := getBlockchainConfig(ctx, api.Client(), b)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to get config dict: %w\", err)\n\t}\n\tcfgCell, err := tlb.ToCell(cfgDict)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to serialize blockchain config: %w\", err)\n\t}\n\tconfigCell, err := boc.DeserializeBoc(cfgCell.ToBOC())\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to deserialize blockchain config: %w\", err)\n\t}\n\tif len(configCell) != 1 {\n\t\treturn nil, nil, fmt.Errorf(\"blockchain config must conatins only one cell\")\n\t}\n\n\tdata := cfg.Get(4)\n\tif data == nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to get root address from blockchain config\")\n\t}\n\n\thash, err := data.BeginParse().LoadSlice(256)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to get root address from blockchain config 4, failed to load hash: %w\", err)\n\t}\n\n\treturn address.NewAddress(0, 255, hash), configCell[0], nil\n}\n\n// GenerateDefaultWallet generates HighloadV2R2 or V3R2 TON wallet with\n// default subwallet_id and returns wallet, shard and subwalletID\nfunc (c *Connection) GenerateDefaultWallet(seed string, isHighload bool) (\n\tw *wallet.Wallet,\n\tshard byte,\n\tsubwalletID uint32, err error,\n) {\n\twords := strings.Split(seed, \" \")\n\tif isHighload {\n\t\tw, err = wallet.FromSeed(c, words, wallet.HighloadV2R2)\n\t} else {\n\t\tw, err = wallet.FromSeed(c, words, wallet.V3)\n\t}\n\tif err != nil {\n\t\treturn nil, 0, 0, err\n\t}\n\treturn w, w.Address().Data()[0], uint32(wallet.DefaultSubwallet), nil\n}\n\n// GenerateSubWallet generates subwallet for custom shard and\n// subwallet_id >= startSubWalletId and returns wallet and new subwallet_id\nfunc (c *Connection) GenerateSubWallet(seed string, shard byte, startSubWalletID uint32) (*wallet.Wallet, uint32, error) {\n\twords := strings.Split(seed, \" \")\n\tbasic, err := wallet.FromSeed(c, words, wallet.V3)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tfor id := startSubWalletID; id < math.MaxUint32; id++ {\n\t\tsubWallet, err := basic.GetSubwallet(id)\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\taddr, err := core.AddressFromTonutilsAddress(subWallet.Address())\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tif inShard(addr, shard) {\n\t\t\treturn subWallet, id, nil\n\t\t}\n\t}\n\treturn nil, 0, fmt.Errorf(\"subwallet not found\")\n}\n\n// GetJettonWalletAddress generates jetton wallet address from owner and jetton master addresses\nfunc (c *Connection) GetJettonWalletAddress(\n\tctx context.Context,\n\towner *address.Address,\n\tjettonMaster *address.Address,\n) (*address.Address, error) {\n\tcontr, err := c.getContract(ctx, jettonMaster)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\temulator, err := newEmulator(contr.Code, contr.Data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\taddr, err := getJettonWalletAddressByTVM(owner, contr.Address, emulator)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := addr.ToTonutilsAddressStd(0)\n\tres.SetTestnetOnly(config.Config.Testnet)\n\treturn res, nil\n}\n\nfunc (c *Connection) GetJettonBalanceByOwner(\n\tctx context.Context,\n\towner *address.Address,\n\tjettonMaster *address.Address,\n) (*big.Int, error) {\n\n\tjettonMasterClient := jetton.NewJettonMasterClient(c.client, jettonMaster)\n\n\tjettonWalletClient, err := jettonMasterClient.GetJettonWallet(ctx, owner)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn jettonWalletClient.GetBalance(ctx)\n}\n\nfunc (c *Connection) DnsResolveSmc(\n\tctx context.Context,\n\tdomainName string,\n) (*address.Address, error) {\n\n\t// TODO: it is necessary to distinguish network errors from the impossibility of resolving\n\tdomain, err := c.resolver.Resolve(ctx, domainName)\n\tif errors.Is(err, dns.ErrNoSuchRecord) {\n\t\treturn nil, core.ErrNotFound\n\t} else if err != nil {\n\t\treturn nil, err\n\t}\n\n\t// TODO: replace after tonutils fix\n\tsmcAddr := getWalletRecord(domain)\n\n\tif smcAddr == nil {\n\t\t// not wallet\n\t\treturn nil, core.ErrNotFound\n\t}\n\n\treturn smcAddr, nil\n}\n\nfunc getWalletRecord(d *dns.Domain) *address.Address {\n\n\t// TODO: remove after tonutils fix\n\trec := d.GetRecord(\"wallet\")\n\tif rec == nil {\n\t\treturn nil\n\t}\n\tp := rec.BeginParse()\n\n\tp, err := p.LoadRef()\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tcategory, err := p.LoadUInt(16)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tif category != 0x9fd3 { // const _CategoryContractAddr = 0x9fd3\n\t\treturn nil\n\t}\n\n\taddr, err := p.LoadAddr()\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn addr\n}\n\n// GenerateDepositJettonWalletForProxy\n// Generates jetton wallet address for custom shard and proxy contract as owner with subwallet_id >= startSubWalletId\nfunc (c *Connection) GenerateDepositJettonWalletForProxy(\n\tctx context.Context,\n\tshard byte,\n\tproxyOwner, jettonMaster *address.Address,\n\tstartSubWalletID uint32,\n) (\n\tproxy *core.JettonProxy,\n\taddr *address.Address,\n\terr error,\n) {\n\tcontr, err := c.getContract(ctx, jettonMaster)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\temulator, err := newEmulator(contr.Code, contr.Data)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tfor id := startSubWalletID; id < math.MaxUint32; id++ {\n\t\tproxy, err = core.NewJettonProxy(id, proxyOwner)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tjettonWalletAddress, err := getJettonWalletAddressByTVM(proxy.Address(), contr.Address, emulator)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tif inShard(jettonWalletAddress, shard) {\n\t\t\taddr = jettonWalletAddress.ToTonutilsAddressStd(0)\n\t\t\taddr.SetTestnetOnly(config.Config.Testnet)\n\t\t\treturn proxy, addr, nil\n\t\t}\n\t}\n\treturn nil, nil, fmt.Errorf(\"jetton wallet address not found\")\n}\n\nfunc (c *Connection) getContract(ctx context.Context, addr *address.Address) (contract, error) {\n\tblock, err := c.client.CurrentMasterchainInfo(ctx)\n\tif err != nil {\n\t\treturn contract{}, err\n\t}\n\taccount, err := c.WaitForBlock(block.SeqNo).GetAccount(ctx, block, addr)\n\tif err != nil {\n\t\treturn contract{}, err\n\t}\n\tif account == nil || account.Code == nil || account.Data == nil {\n\t\treturn contract{}, fmt.Errorf(\"empty account code or data\")\n\t}\n\taccountID, err := tongo.ParseAccountID(addr.String())\n\tif err != nil {\n\t\treturn contract{}, err\n\t}\n\tcodeCell, err := boc.DeserializeBoc(account.Code.ToBOC())\n\tif err != nil {\n\t\treturn contract{}, err\n\t}\n\tif len(codeCell) != 1 {\n\t\treturn contract{}, fmt.Errorf(\"BOC must have only one root\")\n\t}\n\tdataCell, err := boc.DeserializeBoc(account.Data.ToBOC())\n\tif err != nil {\n\t\treturn contract{}, err\n\t}\n\tif len(dataCell) != 1 {\n\t\treturn contract{}, fmt.Errorf(\"BOC must have only one root\")\n\t}\n\treturn contract{\n\t\tAddress: accountID,\n\t\tCode:    codeCell[0],\n\t\tData:    dataCell[0],\n\t}, nil\n}\n\nfunc getJettonWalletAddressByTVM(\n\towner *address.Address,\n\tjettonMaster tongo.AccountID,\n\temulator *tvm.Emulator,\n) (core.Address, error) {\n\townerAccountID, err := tongo.ParseAccountID(owner.String())\n\tif err != nil {\n\t\treturn core.Address{}, err\n\t}\n\tslice, err := tongoTlb.TlbStructToVmCellSlice(ownerAccountID.ToMsgAddress())\n\tif err != nil {\n\t\treturn core.Address{}, err\n\t}\n\n\tcode, result, err := emulator.RunSmcMethod(context.Background(), jettonMaster, \"get_wallet_address\",\n\t\ttongoTlb.VmStack{slice})\n\tif err != nil {\n\t\treturn core.Address{}, err\n\t}\n\tif code != 0 || len(result) != 1 || result[0].SumType != \"VmStkSlice\" {\n\t\treturn core.Address{}, fmt.Errorf(\"tvm execution failed\")\n\t}\n\n\tvar msgAddress tongoTlb.MsgAddress\n\terr = result[0].VmStkSlice.UnmarshalToTlbStruct(&msgAddress)\n\tif err != nil {\n\t\treturn core.Address{}, err\n\t}\n\tif msgAddress.SumType != \"AddrStd\" {\n\t\treturn core.Address{}, fmt.Errorf(\"not std jetton wallet address\")\n\t}\n\tif msgAddress.AddrStd.WorkchainId != core.DefaultWorkchain {\n\t\treturn core.Address{}, fmt.Errorf(\"not default workchain for jetton wallet address\")\n\t}\n\treturn core.Address(msgAddress.AddrStd.Address), nil\n}\n\nfunc newEmulator(code, data *boc.Cell) (*tvm.Emulator, error) {\n\temulator, err := tvm.NewEmulator(code, data, config.Config.BlockchainConfig, tvm.WithBalance(1_000_000_000))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// TODO: try tvm.WithLazyC7Optimization()\n\treturn emulator, nil\n}\n\n// GetJettonBalance\n// Get method get_wallet_data() returns (int balance, slice owner, slice jetton, cell jetton_wallet_code)\n// Returns jetton balance for custom block in basic units\nfunc (c *Connection) GetJettonBalance(ctx context.Context, address core.Address, blockID *ton.BlockIDExt) (*big.Int, error) {\n\tjettonWallet := address.ToTonutilsAddressStd(0)\n\tstack, err := c.RunGetMethod(ctx, blockID, jettonWallet, \"get_wallet_data\")\n\tif err != nil {\n\t\tif strings.Contains(err.Error(), \"contract is not initialized\") {\n\t\t\treturn big.NewInt(0), nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"get wallet data error: %v\", err)\n\t}\n\tres := stack.AsTuple()\n\tif len(res) != 4 {\n\t\treturn nil, fmt.Errorf(\"invalid stack size\")\n\t}\n\tswitch res[0].(type) {\n\tcase *big.Int:\n\t\treturn res[0].(*big.Int), nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid balance type\")\n\t}\n}\n\n// GetLastJettonBalance\n// Returns jetton balance for last block in basic units\nfunc (c *Connection) GetLastJettonBalance(ctx context.Context, address *address.Address) (*big.Int, error) {\n\tmasterID, err := c.client.CurrentMasterchainInfo(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\taddr, err := core.AddressFromTonutilsAddress(address)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn c.GetJettonBalance(ctx, addr, masterID)\n}\n\n// GetAccountCurrentState\n// Returns TON balance in nanoTONs and account status\nfunc (c *Connection) GetAccountCurrentState(ctx context.Context, address *address.Address) (*big.Int, tlb.AccountStatus, error) {\n\tmasterID, err := c.client.CurrentMasterchainInfo(ctx)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\t// TODO: fix waitForBlock and 651 error\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, \"\", core.ErrTimeoutExceeded\n\t\tdefault:\n\t\t\taccount, err := c.client.GetAccount(ctx, masterID, address)\n\t\t\tif err != nil && isNotReadyError(err) {\n\t\t\t\ttime.Sleep(time.Millisecond * 200)\n\t\t\t\tcontinue\n\t\t\t} else if err != nil {\n\t\t\t\treturn nil, \"\", err\n\t\t\t}\n\t\t\tif !account.IsActive {\n\t\t\t\treturn big.NewInt(0), tlb.AccountStatusNonExist, nil\n\t\t\t}\n\t\t\treturn account.State.Balance.Nano(), account.State.Status, nil\n\t\t}\n\t}\n}\n\n// DeployTonWallet\n// Deploys wallet contract and wait its activation\nfunc (c *Connection) DeployTonWallet(ctx context.Context, wallet *wallet.Wallet) error {\n\tbalance, status, err := c.GetAccountCurrentState(ctx, wallet.Address())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif balance.Cmp(big.NewInt(0)) == 0 {\n\t\treturn fmt.Errorf(\"empty balance\")\n\t}\n\tif status != tlb.AccountStatusActive {\n\t\terr = wallet.TransferNoBounce(ctx, wallet.Address(), tlb.FromNanoTONU(0), \"\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\treturn nil\n\t}\n\treturn c.WaitStatus(ctx, wallet.Address(), tlb.AccountStatusActive)\n}\n\n// GetTransactionIDsFromBlock\n// Gets all transactions IDs from custom block\nfunc (c *Connection) GetTransactionIDsFromBlock(ctx context.Context, blockID *ton.BlockIDExt) ([]ton.TransactionShortInfo, error) {\n\tvar (\n\t\ttxIDList []ton.TransactionShortInfo\n\t\tafter    *ton.TransactionID3\n\t\tnext     = true\n\t)\n\tfor next {\n\t\tfetchedIDs, more, err := c.client.GetBlockTransactionsV2(ctx, blockID, 256, after)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttxIDList = append(txIDList, fetchedIDs...)\n\t\tnext = more\n\t\tif more {\n\t\t\t// set load offset for next query (pagination)\n\t\t\tafter = fetchedIDs[len(fetchedIDs)-1].ID3()\n\t\t}\n\t}\n\t// sort by LT\n\tsort.Slice(txIDList, func(i, j int) bool {\n\t\treturn txIDList[i].LT < txIDList[j].LT\n\t})\n\treturn txIDList, nil\n}\n\n// GetTransactionFromBlock\n// Gets transaction from block\nfunc (c *Connection) GetTransactionFromBlock(ctx context.Context, blockID *ton.BlockIDExt, txID ton.TransactionShortInfo) (*tlb.Transaction, error) {\n\ttx, err := c.client.GetTransaction(ctx, blockID, address.NewAddress(0, byte(blockID.Workchain), txID.Account), txID.LT)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn tx, nil\n}\n\nfunc inShard(addr core.Address, shard byte) bool {\n\treturn addr[0] == shard\n}\n\nfunc (c *Connection) getCurrentNodeTime(ctx context.Context) (time.Time, error) {\n\tt, err := c.client.GetTime(ctx)\n\tif err != nil {\n\t\treturn time.Time{}, err\n\t}\n\tres := time.Unix(int64(t), 0)\n\treturn res, nil\n}\n\n// CheckTime\n// Checks time diff between node and local time. Due to the fact that the request to the node takes time,\n// the local time is defined as the average between the beginning and end of the request.\n// Returns true if time diff < cutoff.\nfunc (c *Connection) CheckTime(ctx context.Context, cutoff time.Duration) (bool, error) {\n\tprevTime := time.Now()\n\tnodeTime, err := c.getCurrentNodeTime(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tnextTime := time.Now()\n\tmidTime := prevTime.Add(nextTime.Sub(prevTime) / 2)\n\tnodeTimeShift := midTime.Sub(nodeTime)\n\tlog.Infof(\"Service-Node time diff: %v\", nodeTimeShift)\n\tif nodeTimeShift > cutoff || nodeTimeShift < -cutoff {\n\t\treturn false, nil\n\t}\n\treturn true, nil\n}\n\n// WaitStatus\n// Waits custom status for account. Returns error if context timeout is exceeded.\n// Context must be with timeout to avoid blocking!\nfunc (c *Connection) WaitStatus(ctx context.Context, addr *address.Address, status tlb.AccountStatus) error {\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn core.ErrTimeoutExceeded\n\t\tdefault:\n\t\t\t_, st, err := c.GetAccountCurrentState(ctx, addr)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif st == status {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\ttime.Sleep(time.Millisecond * 200)\n\t\t}\n\t}\n}\n\n// tonutils TonAPI interface methods\n\n// GetAccount\n// The method is being redefined for more stable operation.\n// Gets account from prev block if impossible to get it from current block. Be careful with diff calculation between blocks.\nfunc (c *Connection) GetAccount(ctx context.Context, block *ton.BlockIDExt, addr *address.Address) (*tlb.Account, error) {\n\tres, err := c.client.GetAccount(ctx, block, addr)\n\tif err != nil && isNotReadyError(err) {\n\t\tprevBlock, err := c.client.LookupBlock(ctx, block.Workchain, block.Shard, block.SeqNo-1)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn c.client.GetAccount(ctx, prevBlock, addr)\n\t}\n\treturn res, err\n}\n\nfunc (c *Connection) SendExternalMessage(ctx context.Context, msg *tlb.ExternalMessage) error {\n\treturn c.client.SendExternalMessage(ctx, msg)\n}\n\n// RunGetMethod\n// The method is being redefined for more stable operation\n// Wait until BlockIsApplied. Use context with  timeout.\nfunc (c *Connection) RunGetMethod(ctx context.Context, block *ton.BlockIDExt, addr *address.Address, method string, params ...any) (*ton.ExecutionResult, error) {\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, core.ErrTimeoutExceeded\n\t\tdefault:\n\t\t\tres, err := c.client.RunGetMethod(ctx, block, addr, method, params...)\n\t\t\tif err != nil && isNotReadyError(err) {\n\t\t\t\ttime.Sleep(time.Millisecond * 200)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn res, err\n\t\t}\n\t}\n}\n\nfunc (c *Connection) ListTransactions(ctx context.Context, addr *address.Address, num uint32, lt uint64, txHash []byte) ([]*tlb.Transaction, error) {\n\treturn c.client.ListTransactions(ctx, addr, num, lt, txHash)\n}\n\nfunc (c *Connection) Client() ton.LiteClient {\n\treturn c.client.Client()\n}\n\nfunc (c *Connection) CurrentMasterchainInfo(ctx context.Context) (*ton.BlockIDExt, error) {\n\treturn c.client.CurrentMasterchainInfo(ctx)\n}\n\nfunc (c *Connection) GetMasterchainInfo(ctx context.Context) (*ton.BlockIDExt, error) {\n\treturn c.client.GetMasterchainInfo(ctx)\n}\n\nfunc (c *Connection) SendExternalMessageWaitTransaction(ctx context.Context, ext *tlb.ExternalMessage) (*tlb.Transaction, *ton.BlockIDExt, []byte, error) {\n\treturn c.client.SendExternalMessageWaitTransaction(ctx, ext)\n}\n\nfunc getBlockchainConfig(ctx context.Context, client ton.LiteClient, block *ton.BlockIDExt) (*cell.Dictionary, error) {\n\tvar resp tl.Serializable\n\tvar err error\n\terr = client.QueryLiteserver(ctx, ton.GetConfigAll{\n\t\tMode:    0b1111111111,\n\t\tBlockID: block,\n\t}, &resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch t := resp.(type) {\n\tcase ton.ConfigAll:\n\t\tstateExtra, err := ton.CheckShardMcStateExtraProof(block, []*cell.Cell{t.StateProof, t.ConfigProof})\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"incorrect proof: %w\", err)\n\t\t}\n\n\t\treturn stateExtra.ConfigParams.Config.Params, nil\n\tcase ton.LSError:\n\t\treturn nil, t\n\t}\n\treturn nil, fmt.Errorf(\"unexpected response from node\")\n}\n"
  },
  {
    "path": "blockchain/blockchain_test.go",
    "content": "package blockchain\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"github.com/gobicycle/bicycle/core\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"github.com/xssnick/tonutils-go/tlb\"\n\t\"github.com/xssnick/tonutils-go/ton/jetton\"\n\t\"github.com/xssnick/tonutils-go/ton/wallet\"\n\t\"math/big\"\n\t\"math/rand\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar (\n\tjettonMasterAddress, _ = address.ParseAddr(\"kQCKt2WPGX-fh0cIAz38Ljd_OKQjoZE_cqk7QrYGsNP6wfP0\") // TGR in Testnet\n\tactiveAccount, _       = address.ParseAddr(\"kQCOSEttz9aEGXkjd1h_NJsQqOca3T-Pld5zSIPHcYZIxsyf\")\n\tnotActiveAccount, _    = address.ParseAddr(\"kQAkRRJ1RiViVHY2UmUhWCFjdiZBeEYnhkhxI1JTJFNUNG9v\")\n)\n\nfunc connect(t *testing.T) *Connection {\n\tserver := os.Getenv(\"SERVER\")\n\tif server == \"\" {\n\t\tt.Fatal(\"empty server var\")\n\t}\n\tkey := os.Getenv(\"KEY\")\n\tif key == \"\" {\n\t\tt.Fatal(\"empty key var\")\n\t}\n\tc, err := NewConnection(server, key, 100)\n\tif err != nil {\n\t\tt.Fatal(\"connections err: \", err)\n\t}\n\treturn c\n}\n\nfunc getSeed() string {\n\tseed := os.Getenv(\"SEED\")\n\tif seed == \"\" {\n\t\tpanic(\"empty seed\")\n\t}\n\treturn seed\n}\n\nfunc Test_NewConnection(t *testing.T) {\n\tconnect(t)\n}\n\nfunc Test_GenerateDefaultWallet(t *testing.T) {\n\tc := connect(t)\n\tseed := getSeed()\n\thlWallet, shard, id, err := c.GenerateDefaultWallet(seed, false)\n\tif err != nil {\n\t\tt.Fatal(\"gen default wallet err: \", err)\n\t}\n\tif hlWallet.Address().Data()[0] != shard {\n\t\tt.Fatal(\"invalid shard\")\n\t}\n\tif id != wallet.DefaultSubwallet {\n\t\tt.Fatal(\"invalid subwallet ID\")\n\t}\n\tw, shard, id, err := c.GenerateDefaultWallet(seed, true)\n\tif err != nil {\n\t\tt.Fatal(\"gen default wallet err: \", err)\n\t}\n\tif w.Address().Data()[0] != shard {\n\t\tt.Fatal(\"invalid shard\")\n\t}\n\tif id != wallet.DefaultSubwallet {\n\t\tt.Fatal(\"invalid subwallet ID\")\n\t}\n}\n\nfunc Test_GenerateSubWallet(t *testing.T) {\n\tc := connect(t)\n\tseed := getSeed()\n\tfor i := 0; i < 10; i++ {\n\t\tshard := byte(rand.Intn(255))\n\t\tstartSubWalletID := rand.Uint32()\n\t\tsubWallet, subWalletID, err := c.GenerateSubWallet(seed, shard, startSubWalletID)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"gen sub wallet err: \", err)\n\t\t}\n\t\tif subWalletID <= startSubWalletID {\n\t\t\tt.Fatal(\"invalid subwallet ID\")\n\t\t}\n\t\tif subWallet.Address().Data()[0] != shard {\n\t\t\tt.Fatal(\"invalid shard\")\n\t\t}\n\t}\n}\n\nfunc Test_GetJettonWalletAddress(t *testing.T) {\n\tc := connect(t)\n\tseed := getSeed()\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*30)\n\tdefer cancel()\n\towner, _, _, err := c.GenerateDefaultWallet(seed, true)\n\tif err != nil {\n\t\tt.Fatal(\"gen owner wallet err: \", err)\n\t}\n\tjettonWalletAddr, err := c.GetJettonWalletAddress(ctx, owner.Address(), jettonMasterAddress)\n\tif err != nil {\n\t\tt.Fatal(\"get jetton wallet address err: \", err)\n\t}\n\tmaster := jetton.NewJettonMasterClient(c.client, jettonMasterAddress)\n\tjettonWallet, err := master.GetJettonWallet(ctx, owner.Address())\n\tif err != nil {\n\t\tt.Fatal(\"get jetton wallet address by tonutils method err: \", err)\n\t}\n\tif !bytes.Equal(jettonWallet.Address().Data(), jettonWalletAddr.Data()) ||\n\t\tjettonWallet.Address().Workchain() != jettonWalletAddr.Workchain() {\n\t\tt.Fatal(\"invalid jetton wallet address\")\n\t}\n}\n\nfunc Test_GenerateJettonWalletAddressForProxy(t *testing.T) {\n\tc := connect(t)\n\tseed := getSeed()\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*30)\n\tdefer cancel()\n\towner, _, _, err := c.GenerateDefaultWallet(seed, true)\n\tif err != nil {\n\t\tt.Fatal(\"gen owner wallet err: \", err)\n\t}\n\tmaster := jetton.NewJettonMasterClient(c.client, jettonMasterAddress)\n\tfor i := 0; i < 10; i++ {\n\t\tshard := byte(rand.Intn(255))\n\t\tstartSubWalletID := rand.Uint32()\n\t\tproxy, jettonWalletAddr, err := c.GenerateDepositJettonWalletForProxy(ctx, shard, owner.Address(), jettonMasterAddress, startSubWalletID)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"gen sub wallet err: \", err)\n\t\t}\n\t\tif proxy == nil {\n\t\t\tt.Fatal(\"nil owner wallet\")\n\t\t}\n\t\tif jettonWalletAddr == nil {\n\t\t\tt.Fatal(\"nil jetton wallet address\")\n\t\t}\n\t\tif proxy.SubwalletID <= startSubWalletID {\n\t\t\tt.Fatal(\"invalid subwallet ID\")\n\t\t}\n\t\tif jettonWalletAddr.Data()[0] != shard {\n\t\t\tt.Fatal(\"invalid shard\")\n\t\t}\n\t\tjettonWallet, err := master.GetJettonWallet(ctx, proxy.Address())\n\t\tif err != nil {\n\t\t\tt.Fatal(\"get jetton wallet address by tonutils method err: \", err)\n\t\t}\n\t\tif jettonWallet.Address().String() != jettonWalletAddr.String() {\n\t\t\tt.Fatal(\"invalid jetton wallet address\")\n\t\t}\n\t}\n}\n\nfunc Test_GetJettonBalance(t *testing.T) {\n\tc := connect(t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*30)\n\tdefer cancel()\n\tblock, err := c.client.CurrentMasterchainInfo(ctx)\n\tif err != nil {\n\t\tt.Fatal(\"get current masterchain err: \", err)\n\t}\n\tcoreAddr1 := core.AddressMustFromTonutilsAddress(activeAccount)\n\tcoreAddr2 := core.AddressMustFromTonutilsAddress(notActiveAccount)\n\tb1, err := c.GetJettonBalance(ctx, coreAddr1, block)\n\tif err != nil {\n\t\tt.Fatal(\"get balance: \", err)\n\t}\n\tif b1.Cmp(big.NewInt(0)) != 1 {\n\t\tt.Fatal(\"empty balance: \", err)\n\t}\n\tb2, err := c.GetJettonBalance(ctx, coreAddr2, block)\n\tif err != nil {\n\t\tt.Fatal(\"get balance: \", err)\n\t}\n\tif b2.Cmp(big.NewInt(0)) != 0 {\n\t\tt.Fatal(\"not empty balance: \", err)\n\t}\n}\n\nfunc Test_GetAccountCurrentState(t *testing.T) {\n\tc := connect(t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*30)\n\tdefer cancel()\n\tb1, st1, err := c.GetAccountCurrentState(ctx, activeAccount)\n\tif err != nil {\n\t\tt.Fatal(\"get acc current state err: \", err)\n\t}\n\tif b1.Cmp(big.NewInt(0)) != 1 || st1 != tlb.AccountStatusActive {\n\t\tt.Fatal(\"acc not active\")\n\t}\n\tb2, st2, err := c.GetAccountCurrentState(ctx, notActiveAccount)\n\tif err != nil {\n\t\tt.Fatal(\"get acc current state err: \", err)\n\t}\n\tif b2.Cmp(big.NewInt(0)) != 0 || st2 != tlb.AccountStatusNonExist {\n\t\tt.Fatal(\"acc active\")\n\t}\n}\n\nfunc Test_DeployTonWallet(t *testing.T) {\n\tc := connect(t)\n\tseed := getSeed()\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*200)\n\tdefer cancel()\n\tamount := tlb.FromNanoTONU(100_000_000)\n\tmainWallet, _, _, err := c.GenerateDefaultWallet(seed, true)\n\tif err != nil {\n\t\tt.Fatal(\"gen main wallet err: \", err)\n\t}\n\tb, st, err := c.GetAccountCurrentState(ctx, mainWallet.Address())\n\tif err != nil {\n\t\tt.Fatal(\"get acc current state err: \", err)\n\t}\n\tif b.Cmp(amount.Nano()) != 1 || st != tlb.AccountStatusActive {\n\t\tt.Fatal(\"wallet not active\")\n\t}\n\tnewWallet, err := mainWallet.GetSubwallet(rand.Uint32())\n\tif err != nil {\n\t\tt.Fatal(\"gen new wallet err: \", err)\n\t}\n\t//fmt.Printf(\"Main wallet: %v\\n\", mainWallet.Address().String())\n\t//fmt.Printf(\"New wallet: %v\\n\", newWallet.Address().String())\n\t_, st, err = c.GetAccountCurrentState(ctx, newWallet.Address())\n\tif err != nil {\n\t\tt.Fatal(\"get acc current state err: \", err)\n\t}\n\tif st != tlb.AccountStatusNonExist {\n\t\tt.Log(\"wallet not empty\")\n\t\tt.Skip()\n\t}\n\terr = mainWallet.TransferNoBounce(\n\t\tctx,\n\t\tnewWallet.Address(),\n\t\tamount,\n\t\t\"\",\n\t\ttrue,\n\t)\n\tif err != nil {\n\t\tt.Fatal(\"transfer err: \", err)\n\t}\n\terr = c.WaitStatus(ctx, newWallet.Address(), tlb.AccountStatusUninit)\n\tif err != nil {\n\t\tt.Fatal(\"wait uninit err: \", err)\n\t}\n\terr = c.DeployTonWallet(ctx, newWallet)\n\tif err != nil {\n\t\tt.Fatal(\"deploy new wallet err: \", err)\n\t}\n\terr = newWallet.Send(ctx, &wallet.Message{\n\t\tMode: 128 + 32, // 128 + 32 send all and destroy\n\t\tInternalMessage: &tlb.InternalMessage{\n\t\t\tIHRDisabled: true,\n\t\t\tBounce:      false,\n\t\t\tDstAddr:     mainWallet.Address(),\n\t\t\tAmount:      tlb.FromNanoTONU(0),\n\t\t\tBody:        nil,\n\t\t},\n\t}, false)\n\tif err != nil {\n\t\tt.Fatal(\"send withdrawal err: \", err)\n\t}\n\terr = c.WaitStatus(ctx, newWallet.Address(), tlb.AccountStatusNonExist)\n\tif err != nil {\n\t\tt.Fatal(\"wait empty err: \", err)\n\t}\n}\n\nfunc Test_GetTransactionIDsFromBlock(t *testing.T) {\n\tc := connect(t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*30)\n\tdefer cancel()\n\tmasterID, err := c.client.CurrentMasterchainInfo(ctx)\n\tif err != nil {\n\t\tt.Fatal(\"get last block err: \", err)\n\t}\n\t_, err = c.GetTransactionIDsFromBlock(ctx, masterID)\n\tif err != nil {\n\t\tt.Fatal(\"get tx ids err: \", err)\n\t}\n}\n\nfunc Test_GetTransactionFromBlock(t *testing.T) {\n\tc := connect(t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*120)\n\tdefer cancel()\n\tfor {\n\t\tmasterID, err := c.client.CurrentMasterchainInfo(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"get last block err: \", err)\n\t\t}\n\t\ttxIDs, err := c.GetTransactionIDsFromBlock(ctx, masterID)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"get tx ids err: \", err)\n\t\t}\n\t\tif len(txIDs) > 0 {\n\t\t\ttx, err := c.GetTransactionFromBlock(ctx, masterID, txIDs[0])\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"get tx err: \", err)\n\t\t\t}\n\t\t\tif tx == nil {\n\t\t\t\tt.Fatal(\"nil tx\")\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc Test_CheckTime(t *testing.T) {\n\tc := connect(t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*20)\n\tdefer cancel()\n\tres, err := c.CheckTime(ctx, time.Second*0)\n\tif err != nil {\n\t\tt.Fatal(\"check time err: \", err)\n\t}\n\tif res == true {\n\t\tt.Fatal(\"time diff can not be 0\")\n\t}\n\tres, err = c.CheckTime(ctx, time.Hour*1000)\n\tif err != nil {\n\t\tt.Fatal(\"check time err: \", err)\n\t}\n\tif res == false {\n\t\tt.Fatal(\"failed for extra large cutoff\")\n\t}\n}\n\nfunc Test_WaitStatus(t *testing.T) {\n\tc := connect(t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*7)\n\tdefer cancel()\n\terr := c.WaitStatus(ctx, activeAccount, tlb.AccountStatusActive)\n\tif err != nil {\n\t\tt.Fatal(\"wait status err: \", err)\n\t}\n\terr = c.WaitStatus(ctx, activeAccount, tlb.AccountStatusNonExist)\n\tif err == nil {\n\t\tt.Fatal(\"must be timeout error\")\n\t}\n}\n\nfunc Test_GetAccount(t *testing.T) {\n\tc := connect(t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*120)\n\tdefer cancel()\n\tfor i := 0; i < 20; i++ {\n\t\tb, err := c.client.GetMasterchainInfo(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"get masterchain info err: \", err)\n\t\t}\n\t\t_, err = c.GetAccount(ctx, b, activeAccount)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"get account err: \", err)\n\t\t}\n\t}\n}\n\nfunc Test_RunGetMethod(t *testing.T) {\n\tc := connect(t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*120)\n\tdefer cancel()\n\tfor i := 0; i < 20; i++ {\n\t\tb, err := c.client.GetMasterchainInfo(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"get masterchain info err: \", err)\n\t\t}\n\t\t_, err = c.RunGetMethod(ctx, b, jettonMasterAddress, \"get_jetton_data\")\n\t\tif err != nil {\n\t\t\tt.Fatal(\"run get method err: \", err)\n\t\t}\n\t}\n}\n\nfunc Test_NextBlock(t *testing.T) {\n\tc := connect(t)\n\tvar shard byte = 123\n\tst := NewShardTracker(shard, nil, c)\n\tfor i := 0; i < 5; i++ {\n\t\th, _, err := st.NextBlock()\n\t\tif err != nil {\n\t\t\tt.Fatal(\"get next block err: \", err)\n\t\t}\n\t\tif !isInShard(uint64(h.Shard), shard) {\n\t\t\tt.Fatal(\"next block not in shard\")\n\t\t}\n\t}\n}\n\nfunc Test_Stop(t *testing.T) {\n\tc := connect(t)\n\tst := NewShardTracker(123, nil, c)\n\tfor i := 0; i < 2; i++ {\n\t\t_, _, err := st.NextBlock()\n\t\tif err != nil {\n\t\t\tt.Fatal(\"get next block err: \", err)\n\t\t}\n\t}\n\tst.Stop()\n\t_, flag, err := st.NextBlock()\n\tif err != nil {\n\t\tt.Fatal(\"get next block err: \", err)\n\t}\n\tif !flag {\n\t\tt.Fatal(\"no shutdown flag\")\n\t}\n}\n"
  },
  {
    "path": "blockchain/limited_client.go",
    "content": "package blockchain\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/xssnick/tonutils-go/tl\"\n\t\"github.com/xssnick/tonutils-go/ton\"\n\t\"golang.org/x/time/rate\"\n)\n\ntype limitedLiteClient struct {\n\tlimiter  *rate.Limiter\n\toriginal ton.LiteClient\n}\n\nfunc newLimitedClient(lc ton.LiteClient, rateLimit int) *limitedLiteClient {\n\treturn &limitedLiteClient{\n\t\toriginal: lc,\n\t\tlimiter:  rate.NewLimiter(rate.Limit(rateLimit), 1),\n\t}\n}\n\nfunc (w *limitedLiteClient) QueryLiteserver(ctx context.Context, payload tl.Serializable, result tl.Serializable) error {\n\terr := w.limiter.Wait(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"limiter err: %w\", err)\n\t}\n\treturn w.original.QueryLiteserver(ctx, payload, result)\n}\n\nfunc (w *limitedLiteClient) StickyContext(ctx context.Context) context.Context {\n\treturn w.original.StickyContext(ctx)\n}\n\nfunc (w *limitedLiteClient) StickyNodeID(ctx context.Context) uint32 {\n\treturn w.original.StickyNodeID(ctx)\n}\n\nfunc (w *limitedLiteClient) StickyContextNextNode(ctx context.Context) (context.Context, error) {\n\treturn w.original.StickyContextNextNode(ctx)\n}\n\nfunc (w *limitedLiteClient) StickyContextNextNodeBalanced(ctx context.Context) (context.Context, error) {\n\treturn w.original.StickyContextNextNodeBalanced(ctx)\n}\n"
  },
  {
    "path": "blockchain/shard_tracker.go",
    "content": "package blockchain\n\nimport (\n\t\"context\"\n\t\"github.com/gobicycle/bicycle/core\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/xssnick/tonutils-go/tlb\"\n\t\"github.com/xssnick/tonutils-go/ton\"\n\t\"math/bits\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst ErrBlockNotApplied = \"block is not applied\"\nconst ErrBlockNotInDB = \"code 651\"\n\ntype ShardTracker struct {\n\tconnection            *Connection\n\tshard                 byte\n\tlastKnownShardBlock   *ton.BlockIDExt\n\tlastMasterBlock       *ton.BlockIDExt\n\tbuffer                []core.ShardBlockHeader\n\tgracefulShutdown      bool\n\tinfoCounter, infoStep int\n\tinfoLastTime          time.Time\n}\n\n// NewShardTracker creates new tracker to get blocks with specific shard attribute\nfunc NewShardTracker(shard byte, startBlock *ton.BlockIDExt, connection *Connection) *ShardTracker {\n\tt := &ShardTracker{\n\t\tconnection:          connection,\n\t\tshard:               shard,\n\t\tlastKnownShardBlock: startBlock,\n\t\tbuffer:              make([]core.ShardBlockHeader, 0),\n\t\tinfoCounter:         0,\n\t\tinfoStep:            1000,\n\t\tinfoLastTime:        time.Now(),\n\t}\n\treturn t\n}\n\n// NextBlock returns next block header and graceful shutdown flag.\n// (ShardBlockHeader, false) for normal operation and (empty block header, true) for graceful shutdown.\nfunc (s *ShardTracker) NextBlock() (core.ShardBlockHeader, bool, error) {\n\tif s.gracefulShutdown {\n\t\treturn core.ShardBlockHeader{}, true, nil\n\t}\n\th := s.getNext()\n\tif h != nil {\n\t\treturn *h, false, nil\n\t}\n\t// the interval between blocks can be up to 40 seconds\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*60)\n\tdefer cancel()\n\tmasterBlockID, err := s.getNextMasterBlockID(ctx)\n\tif err != nil {\n\t\treturn core.ShardBlockHeader{}, false, err\n\t}\n\texit, err := s.loadShardBlocksBatch(masterBlockID)\n\tif err != nil {\n\t\treturn core.ShardBlockHeader{}, false, err\n\t}\n\tif exit {\n\t\tlog.Printf(\"Shard tracker sync stopped\")\n\t\treturn core.ShardBlockHeader{}, true, nil\n\t}\n\treturn s.NextBlock()\n}\n\n// Stop initiates graceful shutdown\nfunc (s *ShardTracker) Stop() {\n\ts.gracefulShutdown = true\n}\n\nfunc (s *ShardTracker) getNext() *core.ShardBlockHeader {\n\tif len(s.buffer) != 0 {\n\t\th := s.buffer[0]\n\t\ts.buffer = s.buffer[1:]\n\t\treturn &h\n\t}\n\treturn nil\n}\n\nfunc (s *ShardTracker) getNextMasterBlockID(ctx context.Context) (*ton.BlockIDExt, error) {\n\tfor {\n\t\tmasterBlockID, err := s.connection.client.GetMasterchainInfo(ctx)\n\t\tif err != nil {\n\t\t\t// exit by context timeout\n\t\t\treturn nil, err\n\t\t}\n\t\tif s.lastMasterBlock == nil {\n\t\t\ts.lastMasterBlock = masterBlockID\n\t\t\treturn masterBlockID, nil\n\t\t}\n\t\tif masterBlockID.SeqNo == s.lastMasterBlock.SeqNo {\n\t\t\ttime.Sleep(time.Second)\n\t\t\tcontinue\n\t\t}\n\t\ts.lastMasterBlock = masterBlockID\n\t\treturn masterBlockID, nil\n\t}\n}\n\nfunc (s *ShardTracker) loadShardBlocksBatch(masterBlockID *ton.BlockIDExt) (bool, error) {\n\tvar (\n\t\tshards []*ton.BlockIDExt\n\t\terr    error\n\t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*20)\n\tdefer cancel()\n\tfor {\n\t\tshards, err = s.connection.client.GetBlockShardsInfo(ctx, masterBlockID)\n\t\tif err != nil && isNotReadyError(err) { // TODO: clarify error type\n\t\t\ttime.Sleep(time.Second)\n\t\t\tcontinue\n\t\t} else if err != nil {\n\t\t\treturn false, err\n\t\t\t// exit by context timeout\n\t\t}\n\t\tbreak\n\t}\n\ts.infoCounter = 0\n\tbatch, exit, err := s.getShardBlocksRecursively(filterByShard(shards, s.shard), nil)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif exit {\n\t\treturn true, nil\n\t}\n\tif len(batch) != 0 {\n\t\ts.lastKnownShardBlock = batch[0].BlockIDExt\n\t\tfor i := len(batch) - 1; i >= 0; i-- {\n\t\t\ts.buffer = append(s.buffer, batch[i])\n\t\t}\n\t}\n\treturn false, nil\n}\n\nfunc (s *ShardTracker) getShardBlocksRecursively(i *ton.BlockIDExt, batch []core.ShardBlockHeader) ([]core.ShardBlockHeader, bool, error) {\n\tif s.gracefulShutdown {\n\t\treturn nil, true, nil\n\t}\n\tif s.lastKnownShardBlock == nil {\n\t\ts.lastKnownShardBlock = i\n\t}\n\tisKnown := (s.lastKnownShardBlock.Shard == i.Shard) && (s.lastKnownShardBlock.SeqNo == i.SeqNo)\n\tif isKnown {\n\t\treturn batch, false, nil\n\t}\n\n\t// compare seqno with filtered shard block\n\t// handle the case when a node may reference an old block\n\tif s.lastKnownShardBlock.SeqNo > i.SeqNo {\n\t\treturn []core.ShardBlockHeader{}, false, nil\n\t}\n\n\tseqnoDiff := int(i.SeqNo - s.lastKnownShardBlock.SeqNo)\n\tif seqnoDiff > s.infoStep {\n\t\tif s.infoCounter%s.infoStep == 0 {\n\t\t\testimatedTime := time.Duration(seqnoDiff/s.infoStep) * time.Since(s.infoLastTime)\n\t\t\ts.infoLastTime = time.Now()\n\t\t\tif s.infoCounter == 0 {\n\t\t\t\tlog.Printf(\"Shard tracker syncing... Seqno diff: %v Estimated time: unknown\\n\", seqnoDiff)\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"Shard tracker syncing... Seqno diff: %v Estimated time: %v\\n\", seqnoDiff, estimatedTime)\n\t\t\t}\n\t\t}\n\t\ts.infoCounter++\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*20)\n\tdefer cancel()\n\th, err := s.connection.getShardBlocksHeader(ctx, i, s.shard)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\tbatch = append(batch, h)\n\treturn s.getShardBlocksRecursively(h.Parent, batch)\n}\n\nfunc isInShard(blockShardPrefix uint64, shard byte) bool {\n\tif blockShardPrefix == 0 {\n\t\tlog.Fatalf(\"invalid shard_prefix\")\n\t}\n\tprefixLen := 64 - 1 - bits.TrailingZeros64(blockShardPrefix) // without one insignificant bit\n\tif prefixLen > 8 {\n\t\tlog.Fatalf(\"more than 256 shards is not supported\")\n\t}\n\tres := (uint64(shard) << (64 - 8)) ^ blockShardPrefix\n\n\treturn bits.LeadingZeros64(res) >= prefixLen\n}\n\nfunc filterByShard(headers []*ton.BlockIDExt, shard byte) *ton.BlockIDExt {\n\tfor _, h := range headers {\n\t\tif isInShard(uint64(h.Shard), shard) {\n\t\t\treturn h\n\t\t}\n\t}\n\tlog.Fatalf(\"must be at least one suitable shard block\")\n\treturn nil\n}\n\nfunc convertBlockToShardHeader(block *tlb.Block, info *ton.BlockIDExt, shard byte) (core.ShardBlockHeader, error) {\n\tparents, err := block.BlockInfo.GetParentBlocks()\n\tif err != nil {\n\t\treturn core.ShardBlockHeader{}, err\n\t}\n\tparent := filterByShard(parents, shard)\n\treturn core.ShardBlockHeader{\n\t\tNotMaster:  block.BlockInfo.NotMaster,\n\t\tGenUtime:   block.BlockInfo.GenUtime,\n\t\tStartLt:    block.BlockInfo.StartLt,\n\t\tEndLt:      block.BlockInfo.EndLt,\n\t\tParent:     parent,\n\t\tBlockIDExt: info,\n\t}, nil\n}\n\n// get shard block header for specific shard attribute with one parent\nfunc (c *Connection) getShardBlocksHeader(ctx context.Context, shardBlockID *ton.BlockIDExt, shard byte) (core.ShardBlockHeader, error) {\n\tvar (\n\t\terr   error\n\t\tblock *tlb.Block\n\t)\n\tfor {\n\t\tblock, err = c.client.GetBlockData(ctx, shardBlockID)\n\t\tif err != nil && isNotReadyError(err) {\n\t\t\ttime.Sleep(time.Millisecond * 500)\n\t\t\tcontinue\n\t\t} else if err != nil {\n\t\t\treturn core.ShardBlockHeader{}, err\n\t\t\t// exit by context timeout\n\t\t}\n\t\tbreak\n\t}\n\treturn convertBlockToShardHeader(block, shardBlockID, shard)\n}\n\nfunc isNotReadyError(err error) bool {\n\treturn strings.Contains(err.Error(), ErrBlockNotApplied) || strings.Contains(err.Error(), ErrBlockNotInDB)\n}\n"
  },
  {
    "path": "cmd/processor/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/gobicycle/bicycle/api\"\n\t\"github.com/gobicycle/bicycle/blockchain\"\n\t\"github.com/gobicycle/bicycle/config\"\n\t\"github.com/gobicycle/bicycle/core\"\n\t\"github.com/gobicycle/bicycle/db\"\n\t\"github.com/gobicycle/bicycle/queue\"\n\t\"github.com/gobicycle/bicycle/webhook\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n)\n\nvar Version = \"dev\"\n\nfunc main() {\n\n\tlog.Infof(\"App version: %s\", Version)\n\n\tconfig.GetConfig()\n\n\tsigChannel := make(chan os.Signal, 1)\n\tsignal.Notify(sigChannel, os.Interrupt, syscall.SIGTERM)\n\twg := new(sync.WaitGroup)\n\n\tbcClient, err := blockchain.NewConnection(config.Config.LiteServer, config.Config.LiteServerKey, config.Config.LiteServerRateLimit)\n\tif err != nil {\n\t\tlog.Fatalf(\"blockchain connection error: %v\", err)\n\t}\n\n\tdbClient, err := db.NewConnection(config.Config.DatabaseURI)\n\tif err != nil {\n\t\tlog.Fatalf(\"DB connection error: %v\", err)\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*120)\n\tdefer cancel()\n\n\terr = dbClient.LoadAddressBook(ctx)\n\tif err != nil {\n\t\tlog.Fatalf(\"address book loading error: %v\", err)\n\t}\n\n\tisTimeSynced, err := bcClient.CheckTime(ctx, config.AllowableServiceToNodeTimeDiff)\n\tif err != nil {\n\t\tlog.Fatalf(\"get node time err: %v\", err)\n\t}\n\tif !isTimeSynced {\n\t\tlog.Fatalf(\"Service and Node time not synced\")\n\t}\n\n\twallets, err := core.InitWallets(ctx, dbClient, bcClient, config.Config.Seed, config.Config.Jettons)\n\tif err != nil {\n\t\tlog.Fatalf(\"Wallets initialization error: %v\", err)\n\t}\n\n\tvar notificators []core.Notificator\n\n\tif config.Config.QueueEnabled {\n\t\tqueueClient, err := queue.NewAmqpClient(config.Config.QueueURI, config.Config.QueueEnabled, config.Config.QueueName)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"new queue client creating error: %v\", err)\n\t\t}\n\t\tnotificators = append(notificators, queueClient)\n\t}\n\n\tif config.Config.WebhookEndpoint != \"\" {\n\t\twebhookClient, err := webhook.NewWebhookClient(config.Config.WebhookEndpoint, config.Config.WebhookToken)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"new webhook client creating error: %v\", err)\n\t\t}\n\t\tnotificators = append(notificators, webhookClient)\n\t}\n\n\tvar tracker *blockchain.ShardTracker\n\tblock, err := dbClient.GetLastSavedBlockID(ctx)\n\tif !errors.Is(err, core.ErrNotFound) && err != nil {\n\t\tlog.Fatalf(\"Get last saved block error: %v\", err)\n\t} else if errors.Is(err, core.ErrNotFound) {\n\t\ttracker = blockchain.NewShardTracker(wallets.Shard, nil, bcClient)\n\t} else {\n\t\ttracker = blockchain.NewShardTracker(wallets.Shard, block, bcClient)\n\t}\n\n\tblockScanner := core.NewBlockScanner(wg, dbClient, bcClient, wallets.Shard, tracker, notificators)\n\n\twithdrawalsProcessor := core.NewWithdrawalsProcessor(\n\t\twg, dbClient, bcClient, wallets, config.Config.ColdWallet)\n\twithdrawalsProcessor.Start()\n\n\tapiMux := http.NewServeMux()\n\th := api.NewHandler(dbClient, bcClient, config.Config.APIToken, wallets.Shard, *wallets.TonHotWallet.Address())\n\tapi.RegisterHandlers(apiMux, h)\n\tgo func() {\n\t\terr := http.ListenAndServe(fmt.Sprintf(\":%d\", config.Config.APIPort), apiMux)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"api error: %v\", err)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\t<-sigChannel\n\t\tlog.Printf(\"SIGTERM received\")\n\t\tblockScanner.Stop()\n\t\twithdrawalsProcessor.Stop()\n\t}()\n\n\twg.Wait()\n}\n"
  },
  {
    "path": "cmd/testutil/http.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/gobicycle/bicycle/api\"\n\t\"github.com/gobicycle/bicycle/config\"\n\t\"github.com/gobicycle/bicycle/core\"\n\t\"github.com/gofrs/uuid\"\n\t\"github.com/shopspring/decimal\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"time\"\n)\n\ntype Client struct {\n\tclient *http.Client\n\turlA   string\n\turlB   string\n\ttoken  string\n\tuserID string\n}\n\nfunc NewClient(urlA, urlB, token, userID string) *Client {\n\tc := &Client{\n\t\tclient: &http.Client{Timeout: 10 * time.Second},\n\t\turlA:   urlA,\n\t\turlB:   urlB,\n\t\ttoken:  token,\n\t\tuserID: userID,\n\t}\n\treturn c\n}\n\nfunc (s *Client) InitDeposits(host string) (map[string][]string, error) {\n\tdeposits := make(map[string][]string)\n\taddr, err := s.GetAllAddresses(host)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(addr.Addresses) != 0 {\n\t\tfor _, wa := range addr.Addresses {\n\t\t\tdeposits[wa.Currency] = append(deposits[wa.Currency], wa.Address)\n\t\t}\n\t\treturn deposits, nil\n\t}\n\tfor i := 0; i < depositsQty; i++ {\n\t\taddr, err := s.GetNewAddress(host, core.TonSymbol)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdeposits[core.TonSymbol] = append(deposits[core.TonSymbol], addr)\n\t}\n\tfor i := 0; i < depositsQty; i++ {\n\t\tfor cur := range config.Config.Jettons {\n\t\t\taddr, err := s.GetNewAddress(host, cur)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tdeposits[cur] = append(deposits[cur], addr)\n\t\t}\n\t}\n\tlog.Printf(\"Deposits initialized for %s\", host)\n\treturn deposits, nil\n}\n\nfunc (s *Client) GetAllAddresses(host string) (api.GetAddressesResponse, error) {\n\turl := fmt.Sprintf(\"http://%s/v1/address/all?user_id=%s\", host, s.userID)\n\trequest, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn api.GetAddressesResponse{}, err\n\t}\n\trequest.Header.Add(\"Authorization\", \"Bearer \"+s.token)\n\tresponse, err := s.client.Do(request)\n\tif err != nil {\n\t\treturn api.GetAddressesResponse{}, err\n\t}\n\tdefer func() {\n\t\terr := response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"response body close error: %v\", err)\n\t\t}\n\t}()\n\tif response.StatusCode >= 300 {\n\t\treturn api.GetAddressesResponse{}, fmt.Errorf(\"response status: %v\", response.Status)\n\t}\n\tcontent, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn api.GetAddressesResponse{}, err\n\t}\n\tvar res api.GetAddressesResponse\n\terr = json.Unmarshal(content, &res)\n\tif err != nil {\n\t\treturn api.GetAddressesResponse{}, err\n\t}\n\treturn res, nil\n}\n\nfunc (s *Client) SendWithdrawal(host, currency, destination string, amount int64) (api.WithdrawalResponse, uuid.UUID, error) {\n\turl := fmt.Sprintf(\"http://%s/v1/withdrawal/send\", host)\n\tu, err := uuid.NewV4()\n\tif err != nil {\n\t\treturn api.WithdrawalResponse{}, uuid.UUID{}, err\n\t}\n\treqData := api.WithdrawalRequest{\n\t\tUserID:      s.userID,\n\t\tQueryID:     u.String(),\n\t\tCurrency:    currency,\n\t\tAmount:      decimal.New(amount, 0),\n\t\tDestination: destination,\n\t\tComment:     u.String(),\n\t}\n\tjsonData, err := json.Marshal(reqData)\n\tif err != nil {\n\t\treturn api.WithdrawalResponse{}, uuid.UUID{}, err\n\t}\n\trequest, err := http.NewRequest(\"POST\", url, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn api.WithdrawalResponse{}, uuid.UUID{}, err\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json; charset=UTF-8\")\n\trequest.Header.Add(\"Authorization\", \"Bearer \"+s.token)\n\tresponse, err := s.client.Do(request)\n\tif err != nil {\n\t\treturn api.WithdrawalResponse{}, uuid.UUID{}, err\n\t}\n\tdefer func() {\n\t\terr := response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"response body close error: %v\", err)\n\t\t}\n\t}()\n\tif response.StatusCode >= 300 {\n\t\treturn api.WithdrawalResponse{}, uuid.UUID{}, fmt.Errorf(\"response status: %v\", response.Status)\n\t}\n\tcontent, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn api.WithdrawalResponse{}, uuid.UUID{}, err\n\t}\n\tvar res api.WithdrawalResponse\n\terr = json.Unmarshal(content, &res)\n\tif err != nil {\n\t\treturn api.WithdrawalResponse{}, uuid.UUID{}, err\n\t}\n\treturn res, u, nil\n}\n\nfunc (s *Client) GetNewAddress(host, currency string) (string, error) {\n\turl := fmt.Sprintf(\"http://%s/v1/address/new\", host)\n\treqData := struct {\n\t\tUserID   string `json:\"user_id\"`\n\t\tCurrency string `json:\"currency\"`\n\t}{\n\t\tUserID:   s.userID,\n\t\tCurrency: currency,\n\t}\n\tjsonData, err := json.Marshal(reqData)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\trequest, err := http.NewRequest(\"POST\", url, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json; charset=UTF-8\")\n\trequest.Header.Add(\"Authorization\", \"Bearer \"+s.token)\n\tresponse, err := s.client.Do(request)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer func() {\n\t\terr := response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"response body close error: %v\", err)\n\t\t}\n\t}()\n\tif response.StatusCode >= 300 {\n\t\treturn \"\", fmt.Errorf(\"response status: %v\", response.Status)\n\t}\n\tcontent, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar res struct {\n\t\tAddress string `json:\"address\"`\n\t}\n\terr = json.Unmarshal(content, &res)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn res.Address, nil\n}\n\nfunc (s *Client) GetWithdrawalStatus(host string, id int64) (api.WithdrawalStatusResponse, error) {\n\turl := fmt.Sprintf(\"http://%s/v1/withdrawal/status?id=%v\", host, id)\n\trequest, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn api.WithdrawalStatusResponse{}, err\n\t}\n\trequest.Header.Add(\"Authorization\", \"Bearer \"+s.token)\n\tresponse, err := s.client.Do(request)\n\tif err != nil {\n\t\treturn api.WithdrawalStatusResponse{}, err\n\t}\n\tdefer func() {\n\t\terr := response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"response body close error: %v\", err)\n\t\t}\n\t}()\n\tif response.StatusCode >= 300 {\n\t\treturn api.WithdrawalStatusResponse{}, fmt.Errorf(\"response status: %v\", response.Status)\n\t}\n\tcontent, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn api.WithdrawalStatusResponse{}, err\n\t}\n\tvar res api.WithdrawalStatusResponse\n\terr = json.Unmarshal(content, &res)\n\tif err != nil {\n\t\treturn api.WithdrawalStatusResponse{}, err\n\t}\n\treturn res, nil\n}\n"
  },
  {
    "path": "cmd/testutil/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"github.com/gobicycle/bicycle/blockchain\"\n\t\"github.com/gobicycle/bicycle/config\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\nvar (\n\tonlyMonitoring = true\n\tVersion        = \"dev\"\n)\n\nconst (\n\tdepositsQty          = 10\n\ttonWithdrawAmount    = 550_000_000\n\tjettonWithdrawAmount = 550_000_000\n\ttonMinCutoff         = 10_000_000_000\n)\n\nfunc main() {\n\n\tlog.Printf(\"App version: %s\", Version)\n\n\tconfig.GetConfig()\n\tif circulation := os.Getenv(\"CIRCULATION\"); circulation == \"true\" {\n\t\tonlyMonitoring = false\n\t}\n\n\turlA := os.Getenv(\"HOST_A\")\n\tif urlA == \"\" {\n\t\tlog.Fatalf(\"empty HOST_A env var\")\n\t}\n\turlB := os.Getenv(\"HOST_B\")\n\tif urlB == \"\" {\n\t\tlog.Fatalf(\"empty HOST_B env var\")\n\t}\n\n\thotWalletA, err := address.ParseAddr(os.Getenv(\"HOT_WALLET_A\"))\n\tif err != nil {\n\t\tlog.Fatalf(\"invalid HOT_WALLET_A env var\")\n\t}\n\thotWalletB, err := address.ParseAddr(os.Getenv(\"HOT_WALLET_B\"))\n\tif err != nil {\n\t\tlog.Fatalf(\"invalid HOT_WALLET_B env var\")\n\t}\n\n\tbcClient, err := blockchain.NewConnection(config.Config.LiteServer, config.Config.LiteServerKey, config.Config.LiteServerRateLimit)\n\tif err != nil {\n\t\tlog.Fatalf(\"blockchain connection error: %v\", err)\n\t}\n\n\thttpClient := NewClient(urlA, urlB, config.Config.APIToken, \"TestClient\")\n\n\thttp.Handle(\"/metrics\", promhttp.Handler())\n\tgo func() {\n\t\tlog.Fatal(http.ListenAndServe(\":9101\", nil))\n\t}()\n\n\tdepositsA, err := httpClient.InitDeposits(urlA)\n\tif err != nil {\n\t\tlog.Fatalf(\"can not init deposits: %v\", err)\n\t}\n\n\tdepositsB, err := httpClient.InitDeposits(urlB)\n\tif err != nil {\n\t\tlog.Fatalf(\"can not init deposits: %v\", err)\n\t}\n\n\tpayerProc := NewPayerProcessor(context.TODO(), httpClient, bcClient, depositsA, depositsB, hotWalletA, hotWalletB)\n\tpayerProc.Start()\n\n\tfor {\n\t\ttime.Sleep(time.Hour)\n\t}\n}\n"
  },
  {
    "path": "cmd/testutil/metrics.go",
    "content": "package main\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nvar (\n\thotWalletABalance = promauto.NewGaugeVec(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"hot_wallet_a_balance\",\n\t\t\tHelp: \"Hot wallet A balance\",\n\t\t},\n\t\t[]string{\"currency\"},\n\t)\n\thotWalletBBalance = promauto.NewGaugeVec(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"hot_wallet_b_balance\",\n\t\t\tHelp: \"Hot wallet B balance\",\n\t\t},\n\t\t[]string{\"currency\"},\n\t)\n\tdepositWalletABalance = promauto.NewGaugeVec(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"deposit_wallet_a_balance\",\n\t\t\tHelp: \"Deposit wallet A balance\",\n\t\t},\n\t\t[]string{\"currency\", \"address\"},\n\t)\n\tdepositWalletBBalance = promauto.NewGaugeVec(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"deposit_wallet_b_balance\",\n\t\t\tHelp: \"Deposit wallet B balance\",\n\t\t},\n\t\t[]string{\"currency\", \"address\"},\n\t)\n\ttotalBalance = promauto.NewGaugeVec(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"total_balance\",\n\t\t\tHelp: \"Total balance\",\n\t\t},\n\t\t[]string{\"currency\"},\n\t)\n\ttotalLosses = promauto.NewGaugeVec(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"total_losses\",\n\t\t\tHelp: \"Total losses\",\n\t\t},\n\t\t[]string{\"currency\"},\n\t)\n\tpredictedTonLoss = promauto.NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"predicted_ton_loss\",\n\t\t\tHelp: \"Predicted TON loss\",\n\t\t},\n\t)\n\ttotalProcessedAmount = promauto.NewGaugeVec(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"total_processed_amount\",\n\t\t\tHelp: \"Total processed amount\",\n\t\t},\n\t\t[]string{\"currency\"},\n\t)\n)\n"
  },
  {
    "path": "cmd/testutil/utils.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/gobicycle/bicycle/blockchain\"\n\t\"github.com/gobicycle/bicycle/config\"\n\t\"github.com/gobicycle/bicycle/core\"\n\t\"github.com/gofrs/uuid\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"github.com/xssnick/tonutils-go/tlb\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype PayerProcessor struct {\n\tclient                   *Client\n\tbcClient                 *blockchain.Connection\n\tdepositsA                []Deposit\n\tdepositsB                []Deposit\n\thotWalletsAddrA          map[string]*address.Address\n\thotWalletsAddrB          map[string]*address.Address\n\tlastTxIDA, lastTxIDB     TxID\n\tbalances                 *hotBalances\n\tknownUUIDsA, knownUUIDsB map[uuid.UUID]struct{}\n}\n\nconst (\n\tTonLossForJettonExternalWithdrawal int64 = 58_376_225 // without Jetton wallet deploy except of excess // SCALE:69_579_403 TGR:47_173_046\n\tTonLossForJettonInternalWithdrawal int64 = 55_306_226 // proxy deploy without Jetton wallet deploy except of JettonForwardAmount // SCALE:57_873_578 TGR:52_738_873\n\tTonLossForTonExternalWithdrawal    int64 = 10_232_080 // only fees\n\tTonLossForTonInternalWithdrawal    int64 = 8_034_998  // deposit wallet deploy\n)\n\ntype PaymentSide string\n\nconst (\n\tSideA PaymentSide = \"A\"\n\tSideB PaymentSide = \"B\"\n)\n\ntype TxID struct {\n\tLt   uint64\n\tHash []byte\n}\n\ntype Deposit struct {\n\tAddress      *address.Address\n\tJettonWallet *address.Address\n\tCurrency     string\n}\n\ntype hotBalances struct {\n\tmutex               sync.Mutex\n\thotWalletsBalancesA map[string]int64\n\thotWalletsBalancesB map[string]int64\n}\n\nfunc newHotBalances() *hotBalances {\n\treturn &hotBalances{\n\t\thotWalletsBalancesA: make(map[string]int64),\n\t\thotWalletsBalancesB: make(map[string]int64),\n\t}\n}\n\nfunc (h *hotBalances) ReadBalance(walletSide PaymentSide, currency string) int64 {\n\th.mutex.Lock()\n\tdefer h.mutex.Unlock()\n\tswitch walletSide {\n\tcase SideA:\n\t\tb, ok := h.hotWalletsBalancesA[currency]\n\t\tif ok {\n\t\t\treturn b\n\t\t}\n\t\treturn 0\n\tcase SideB:\n\t\tb, ok := h.hotWalletsBalancesB[currency]\n\t\tif ok {\n\t\t\treturn b\n\t\t}\n\t\treturn 0\n\t}\n\tlog.Fatalf(\"invalid payment side\")\n\treturn 0\n}\n\nfunc (h *hotBalances) WriteBalance(walletSide PaymentSide, currency string, balance int64) {\n\th.mutex.Lock()\n\tdefer h.mutex.Unlock()\n\tswitch walletSide {\n\tcase SideA:\n\t\th.hotWalletsBalancesA[currency] = balance\n\t\treturn\n\tcase SideB:\n\t\th.hotWalletsBalancesB[currency] = balance\n\t\treturn\n\t}\n\tlog.Fatalf(\"invalid payment side\")\n}\n\nfunc NewPayerProcessor(\n\tctx context.Context,\n\tclient *Client,\n\tbcClient *blockchain.Connection,\n\tdepositsA map[string][]string,\n\tdepositsB map[string][]string,\n\thotA, hotB *address.Address,\n) *PayerProcessor {\n\t// hot jetton wallets\n\taddrA := make(map[string]*address.Address)\n\taddrB := make(map[string]*address.Address)\n\taddrA[core.TonSymbol] = hotA\n\taddrB[core.TonSymbol] = hotB\n\n\tlastTxIDA, err := getLastTxID(ctx, bcClient, hotA)\n\tif err != nil {\n\t\tlog.Fatalf(\"get last TX ID A error: %v\", err)\n\t}\n\tlastTxIDB, err := getLastTxID(ctx, bcClient, hotB)\n\tif err != nil {\n\t\tlog.Fatalf(\"get last TX ID B error: %v\", err)\n\t}\n\n\tfor cur, jetton := range config.Config.Jettons {\n\t\tjwA, err := bcClient.GetJettonWalletAddress(ctx, hotA, jetton.Master)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"get hot jetton wallet A error: %v\", err)\n\t\t}\n\t\tjwB, err := bcClient.GetJettonWalletAddress(ctx, hotB, jetton.Master)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"get hot jetton wallet B error: %v\", err)\n\t\t}\n\t\taddrA[cur] = jwA\n\t\taddrB[cur] = jwB\n\t\ttotalProcessedAmount.WithLabelValues(cur).Set(0)\n\t}\n\ttotalProcessedAmount.WithLabelValues(core.TonSymbol).Set(0)\n\tp := &PayerProcessor{\n\t\tclient:          client,\n\t\tbcClient:        bcClient,\n\t\tdepositsA:       convertDeposits(bcClient, depositsA),\n\t\tdepositsB:       convertDeposits(bcClient, depositsB),\n\t\thotWalletsAddrA: addrA,\n\t\thotWalletsAddrB: addrB,\n\t\tbalances:        newHotBalances(),\n\t\tlastTxIDA:       lastTxIDA,\n\t\tlastTxIDB:       lastTxIDB,\n\t\tknownUUIDsA:     make(map[uuid.UUID]struct{}),\n\t\tknownUUIDsB:     make(map[uuid.UUID]struct{}),\n\t}\n\treturn p\n}\n\nfunc convertDeposits(bcClient *blockchain.Connection, deposits map[string][]string) []Deposit {\n\tvar dep []Deposit\n\tfor cur, addresses := range deposits {\n\t\tif cur != core.TonSymbol {\n\t\t\tjetton := config.Config.Jettons[cur]\n\t\t\tfor _, a := range addresses {\n\t\t\t\taddr, _ := address.ParseAddr(a)\n\t\t\t\tjw, err := bcClient.GetJettonWalletAddress(context.Background(), addr, jetton.Master)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"get jetton wallet error: %v\", err)\n\t\t\t\t}\n\t\t\t\tdep = append(dep, Deposit{Address: addr, Currency: cur, JettonWallet: jw})\n\t\t\t}\n\t\t} else {\n\t\t\tfor _, a := range addresses {\n\t\t\t\taddr, err := address.ParseAddr(a)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"parse deposit address error: %v\", err)\n\t\t\t\t}\n\t\t\t\tdep = append(dep, Deposit{Address: addr, Currency: cur})\n\t\t\t}\n\t\t}\n\t}\n\treturn dep\n}\n\nfunc (p *PayerProcessor) Start() {\n\tgo p.balanceMonitor()\n\tif !onlyMonitoring {\n\t\tgo p.startPayments(SideA)\n\t\tgo p.startPayments(SideB)\n\t}\n}\n\nfunc (p *PayerProcessor) balanceMonitor() {\n\tlog.Infof(\"Balance monitor started\")\n\tstartTotal := make(map[string]int64)\n\tfor {\n\t\tctx, cancel := context.WithTimeout(context.Background(), time.Second*60)\n\n\t\ttot, err := p.updateHotWalletBalances(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"can not update hot wallet balances: %v\\n\", err)\n\t\t\tcancel()\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, d := range p.depositsA {\n\t\t\tb, err := p.getDepositBalance(ctx, d)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"can not update deposit wallet A balance: %v\\n\", err)\n\t\t\t\tcancel()\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttot[d.Currency] = tot[d.Currency] + b\n\t\t\tdepositWalletABalance.WithLabelValues(d.Currency, d.Address.String()).Set(float64(b))\n\t\t}\n\n\t\tfor _, d := range p.depositsB {\n\t\t\tb, err := p.getDepositBalance(ctx, d)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"can not update deposit wallet B balance: %v\\n\", err)\n\t\t\t\tcancel()\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttot[d.Currency] = tot[d.Currency] + b\n\t\t\tdepositWalletBBalance.WithLabelValues(d.Currency, d.Address.String()).Set(float64(b))\n\t\t}\n\n\t\tfor c, t := range tot {\n\t\t\ttotalBalance.WithLabelValues(c).Set(float64(t))\n\t\t\tif _, ok := startTotal[c]; !ok {\n\t\t\t\tstartTotal[c] = t\n\t\t\t}\n\t\t\ttotalLosses.WithLabelValues(c).Set(float64(t - startTotal[c]))\n\t\t}\n\t\tcancel()\n\t\ttime.Sleep(time.Millisecond * 500)\n\t}\n}\n\nfunc (p *PayerProcessor) updateHotWalletBalances(ctx context.Context) (map[string]int64, error) {\n\ttotBalance := make(map[string]int64)\n\n\tbA, _, err := p.bcClient.GetAccountCurrentState(ctx, p.hotWalletsAddrA[core.TonSymbol])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbB, _, err := p.bcClient.GetAccountCurrentState(ctx, p.hotWalletsAddrB[core.TonSymbol])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thotWalletABalance.WithLabelValues(core.TonSymbol).Set(float64(bA.Int64()))\n\thotWalletBBalance.WithLabelValues(core.TonSymbol).Set(float64(bB.Int64()))\n\ttotBalance[core.TonSymbol] = bA.Int64() + bB.Int64()\n\tp.balances.WriteBalance(SideA, core.TonSymbol, bA.Int64())\n\tp.balances.WriteBalance(SideB, core.TonSymbol, bB.Int64())\n\n\tfor cur := range config.Config.Jettons {\n\t\tbA, err = p.bcClient.GetLastJettonBalance(ctx, p.hotWalletsAddrA[cur])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbB, err = p.bcClient.GetLastJettonBalance(ctx, p.hotWalletsAddrB[cur])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\thotWalletABalance.WithLabelValues(cur).Set(float64(bA.Int64()))\n\t\thotWalletBBalance.WithLabelValues(cur).Set(float64(bB.Int64()))\n\t\ttotBalance[cur] = bA.Int64() + bB.Int64()\n\t\tp.balances.WriteBalance(SideA, cur, bA.Int64())\n\t\tp.balances.WriteBalance(SideB, cur, bB.Int64())\n\t}\n\treturn totBalance, nil\n}\n\nfunc (p *PayerProcessor) getDepositBalance(ctx context.Context, d Deposit) (int64, error) {\n\tif d.Currency == core.TonSymbol {\n\t\tb, _, err := p.bcClient.GetAccountCurrentState(ctx, d.Address)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\treturn b.Int64(), nil\n\t}\n\tb, err := p.bcClient.GetLastJettonBalance(ctx, d.JettonWallet)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn b.Int64(), nil\n}\n\nfunc (p *PayerProcessor) startPayments(side PaymentSide) {\n\tfor {\n\t\ttime.Sleep(time.Second * 60)\n\n\t\tif !p.checkBalances(side) {\n\t\t\tcontinue\n\t\t}\n\n\t\tids, ww, err := p.withdrawToDeposits(side)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"withdraw to deposit error: %s\", err)\n\t\t}\n\t\tlog.Infof(\"Withdrawals sended for side %s\", side)\n\t\terr = p.waitWithdrawals(side, ids)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"wait withdrawals error: %s\", err)\n\t\t}\n\n\t\terr = p.validateWithdrawals(context.TODO(), side, ww)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"validate withdrawals error: %s\", err)\n\t\t}\n\t\tlog.Infof(\"Withdrawals validated for side %s\", side)\n\t}\n}\n\nfunc (p *PayerProcessor) checkBalances(side PaymentSide) bool {\n\ttonRemained := p.balances.ReadBalance(side, core.TonSymbol) - tonWithdrawAmount*depositsQty\n\tif tonRemained < tonMinCutoff {\n\t\treturn false\n\t}\n\tfor cur := range config.Config.Jettons {\n\t\tjettonRemained := p.balances.ReadBalance(side, cur) - jettonWithdrawAmount*depositsQty\n\t\ttonRemained = tonRemained - depositsQty*config.JettonTransferTonAmount.Nano().Int64()\n\t\tif jettonRemained < 0 || tonRemained < tonMinCutoff {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\ntype withdrawal struct {\n\tTo     *address.Address\n\tAmount int64\n\tUUID   uuid.UUID\n}\n\nfunc (p *PayerProcessor) withdrawToDeposits(fromSide PaymentSide) ([]int64, []withdrawal, error) {\n\tvar (\n\t\turl      string\n\t\tdeposits []Deposit\n\t)\n\tswitch fromSide {\n\tcase SideA:\n\t\tdeposits = p.depositsB\n\t\turl = p.client.urlA\n\tcase SideB:\n\t\tdeposits = p.depositsA\n\t\turl = p.client.urlB\n\tdefault:\n\t\treturn nil, nil, fmt.Errorf(\"invalid side\")\n\t}\n\tvar (\n\t\tww  []withdrawal\n\t\tids []int64\n\t)\n\tfor _, d := range deposits {\n\t\tif d.Currency == core.TonSymbol {\n\t\t\tr, u, err := p.client.SendWithdrawal(url, d.Currency, d.Address.String(), tonWithdrawAmount)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\tww = append(ww, withdrawal{\n\t\t\t\tTo:     d.Address,\n\t\t\t\tAmount: tonWithdrawAmount,\n\t\t\t\tUUID:   u,\n\t\t\t})\n\t\t\tids = append(ids, r.ID)\n\t\t\tpredictedTonLoss.Add(predictLoss(core.TonSymbol))\n\t\t\ttotalProcessedAmount.WithLabelValues(core.TonSymbol).Add(float64(tonWithdrawAmount))\n\t\t} else {\n\t\t\tr, u, err := p.client.SendWithdrawal(url, d.Currency, d.Address.String(), jettonWithdrawAmount)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\n\t\t\tww = append(ww, withdrawal{\n\t\t\t\tTo:     d.Address,\n\t\t\t\tAmount: jettonWithdrawAmount,\n\t\t\t\tUUID:   u,\n\t\t\t})\n\t\t\tids = append(ids, r.ID)\n\t\t\tpredictedTonLoss.Add(predictLoss(d.Currency))\n\t\t\ttotalProcessedAmount.WithLabelValues(d.Currency).Add(float64(jettonWithdrawAmount))\n\t\t}\n\t}\n\treturn ids, ww, nil\n}\n\nfunc (p *PayerProcessor) waitWithdrawals(fromSide PaymentSide, ids []int64) error {\n\tvar url string\n\tswitch fromSide {\n\tcase SideA:\n\t\turl = p.client.urlA\n\tcase SideB:\n\t\turl = p.client.urlB\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid side\")\n\t}\n\tcompleted := make(map[int64]struct{})\n\tfor {\n\t\tfor _, id := range ids {\n\t\t\t_, ok := completed[id]\n\t\t\tif ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr, err := p.client.GetWithdrawalStatus(url, id)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif r.Status == core.ProcessedStatus {\n\t\t\t\tcompleted[id] = struct{}{}\n\t\t\t} else {\n\t\t\t\ttime.Sleep(time.Millisecond * 200)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif len(completed) == len(ids) {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *PayerProcessor) validateWithdrawals(ctx context.Context, fromSide PaymentSide, withdrawals []withdrawal) error {\n\tvar (\n\t\taddr     *address.Address\n\t\tlastTxID TxID\n\t\tnewUUIDs []uuid.UUID\n\t)\n\tswitch fromSide {\n\tcase SideA:\n\t\taddr = p.hotWalletsAddrA[core.TonSymbol]\n\t\tlastTxID = p.lastTxIDA\n\tcase SideB:\n\t\taddr = p.hotWalletsAddrB[core.TonSymbol]\n\t\tlastTxID = p.lastTxIDB\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid side\")\n\t}\n\n\ttxs, err := p.loadTXs(ctx, lastTxID, addr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(txs) > 0 {\n\t\tlastTxID = TxID{\n\t\t\tLt:   txs[0].LT,\n\t\t\tHash: txs[0].Hash,\n\t\t}\n\t}\n\n\tremainingTXs := withdrawals\n\tfor _, tx := range txs {\n\t\tww, uuids, err := parseTX(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnewUUIDs = append(newUUIDs, uuids...)\n\t\tremainingTXs = compareWithdrawals(ww, remainingTXs)\n\t}\n\tif len(remainingTXs) > 0 {\n\t\tvar s string\n\t\tfor _, r := range remainingTXs {\n\t\t\ts = s + r.UUID.String() + \", \"\n\t\t}\n\t\treturn fmt.Errorf(\"can not find withdrawals: %s\", s)\n\t}\n\n\tswitch fromSide {\n\tcase SideA:\n\t\tp.lastTxIDA = lastTxID\n\t\terr := checkDoubleSpending(newUUIDs, p.knownUUIDsA)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tcase SideB:\n\t\tp.lastTxIDB = lastTxID\n\t\terr := checkDoubleSpending(newUUIDs, p.knownUUIDsB)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkDoubleSpending(newUUIDs []uuid.UUID, knownUUIDs map[uuid.UUID]struct{}) error {\n\tfor _, u := range newUUIDs {\n\t\t_, ok := knownUUIDs[u]\n\t\tif ok {\n\t\t\treturn fmt.Errorf(\"double spending: %s\", u.String())\n\t\t}\n\t\tknownUUIDs[u] = struct{}{}\n\t}\n\treturn nil\n}\n\nfunc (p *PayerProcessor) loadTXs(ctx context.Context, lastTxID TxID, addr *address.Address) ([]*tlb.Transaction, error) {\n\tnewTxID, err := getLastTxID(ctx, p.bcClient, addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcurrentTxID := newTxID\n\ttxs := make([]*tlb.Transaction, 0)\n\n\tfor {\n\t\t// last transaction has 0 prev lt\n\t\tif currentTxID.Lt == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tlist, err := p.bcClient.ListTransactions(ctx, addr, 3, currentTxID.Lt, currentTxID.Hash)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// oldest = first in list\n\t\tfor i := len(list) - 1; i >= 0; i-- {\n\t\t\tif bytes.Equal(list[i].Hash, lastTxID.Hash) {\n\t\t\t\treturn txs, nil\n\t\t\t}\n\t\t\ttxs = append(txs, list[i])\n\t\t}\n\n\t\t// set previous info from the oldest transaction in list\n\t\tcurrentTxID.Hash = list[0].PrevTxHash\n\t\tcurrentTxID.Lt = list[0].PrevTxLT\n\t}\n\treturn nil, fmt.Errorf(\"can not get txs\")\n}\n\nfunc parseTX(tx *tlb.Transaction) ([]withdrawal, []uuid.UUID, error) {\n\tvar (\n\t\tww      []withdrawal\n\t\tuuids   []uuid.UUID\n\t\tmsgList []tlb.Message\n\t\terr     error\n\t)\n\n\tif tx.OutMsgCount > 0 {\n\t\tmsgList, err = tx.IO.Out.ToSlice()\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\n\tfor _, m := range msgList {\n\t\tif m.MsgType != tlb.MsgTypeInternal {\n\t\t\tcontinue\n\t\t}\n\t\tmsg := m.AsInternal()\n\t\tjt, err := core.DecodeJettonTransfer(msg)\n\t\tif err == nil {\n\t\t\tu, err := uuid.FromString(jt.Comment)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\tww = append(ww, withdrawal{\n\t\t\t\tTo:     jt.Destination,\n\t\t\t\tAmount: jt.Amount.BigInt().Int64(),\n\t\t\t\tUUID:   u,\n\t\t\t})\n\t\t\tuuids = append(uuids, u)\n\t\t} else if msg.Body.BitsSize() > 32 {\n\t\t\tu, err := uuid.FromString(core.LoadComment(msg.Body))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\tww = append(ww, withdrawal{\n\t\t\t\tTo:     msg.DstAddr,\n\t\t\t\tAmount: msg.Amount.Nano().Int64(),\n\t\t\t\tUUID:   u,\n\t\t\t})\n\t\t\tuuids = append(uuids, u)\n\t\t}\n\t}\n\treturn ww, uuids, nil\n}\n\nfunc compareWithdrawals(all, target []withdrawal) []withdrawal {\n\tvar res []withdrawal\n\tfor _, t := range target {\n\t\tfound := false\n\t\tfor _, a := range all {\n\t\t\tif t.UUID == a.UUID {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tres = append(res, t)\n\t\t}\n\t}\n\treturn res\n}\n\nfunc getLastTxID(ctx context.Context, bcClient *blockchain.Connection, address *address.Address) (TxID, error) {\n\tb, err := bcClient.GetMasterchainInfo(ctx)\n\tif err != nil {\n\t\treturn TxID{}, err\n\t}\n\tres, err := bcClient.GetAccount(ctx, b, address)\n\tif err != nil {\n\t\treturn TxID{}, err\n\t}\n\treturn TxID{\n\t\tLt:   res.LastTxLT,\n\t\tHash: res.LastTxHash,\n\t}, nil\n}\n\nfunc predictLoss(currency string) float64 {\n\tif currency == core.TonSymbol {\n\t\tcutoff := config.Config.Ton.Withdrawal.Int64()\n\t\tn := math.Ceil(float64(cutoff) / float64(tonWithdrawAmount)) // number of replenishments of the deposit before withdrawal\n\t\treturn float64(TonLossForTonExternalWithdrawal) + float64(TonLossForTonInternalWithdrawal)/n\n\t} else {\n\t\tcutoff := config.Config.Jettons[currency].WithdrawalCutoff.Int64()\n\t\tn := math.Ceil(float64(cutoff) / float64(jettonWithdrawAmount)) // number of replenishments of the deposit before withdrawal\n\t\treturn float64(TonLossForJettonExternalWithdrawal) + float64(TonLossForJettonInternalWithdrawal)/n\n\t}\n}\n"
  },
  {
    "path": "cmd/testwebhook/main.go",
    "content": "package main\n\nimport (\n\t\"crypto/subtle\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nfunc main() {\n\thttp.HandleFunc(\"/webhook\", getNotification)\n\tfmt.Printf(\"webhook listener started\\n\")\n\terr := http.ListenAndServe(\":3333\", nil)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc getNotification(resp http.ResponseWriter, req *http.Request) {\n\tif req.Method != http.MethodPost {\n\t\tfmt.Printf(\"Not a post request!\\n\")\n\t}\n\tcheckToken(req, \"123\")\n\tres, err := io.ReadAll(req.Body)\n\tif err != nil {\n\t\tfmt.Printf(\"notification read error: %v\", err)\n\t\treturn\n\t}\n\t_ = req.Body.Close()\n\tfmt.Printf(\"Notification: %s\\n\", res)\n\tresp.WriteHeader(http.StatusOK)\n}\n\nfunc checkToken(req *http.Request, token string) {\n\theader := req.Header.Get(\"authorization\")\n\tif header == \"\" {\n\t\tfmt.Printf(\"no authorization header\\n\")\n\t\treturn\n\t}\n\tauth := strings.Split(header, \" \")\n\tif len(auth) != 2 || auth[0] != \"Bearer\" {\n\t\tfmt.Printf(\"not Bearer token\\n\")\n\t\treturn\n\t}\n\tif x := subtle.ConstantTimeCompare([]byte(auth[1]), []byte(token)); x == 1 {\n\t\treturn\n\t} // constant time comparison to prevent time attack\n\tfmt.Printf(\"invalid token\\n\")\n\treturn\n}\n"
  },
  {
    "path": "config/config.go",
    "content": "package config\n\nimport (\n\t\"log\"\n\t\"math/big\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/caarlos0/env/v6\"\n\t\"github.com/shopspring/decimal\"\n\t\"github.com/tonkeeper/tongo/boc\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"github.com/xssnick/tonutils-go/tlb\"\n)\n\nconst MaxJettonForwardTonAmount = 20_000_000\n\nvar (\n\tJettonTransferTonAmount     = tlb.FromNanoTONU(100_000_000)\n\tJettonForwardAmount         = tlb.FromNanoTONU(MaxJettonForwardTonAmount) // must be < JettonTransferTonAmount\n\tJettonInternalForwardAmount = tlb.FromNanoTONU(1)\n\n\tDefaultHotWalletHysteresis = decimal.NewFromFloat(0.95) // `hot_wallet_residual_balance` = `hot_wallet_max_balance` * `hysteresis`\n\n\tExternalMessageLifetime = 50 * time.Second\n\n\tExternalWithdrawalPeriod  = 80 * time.Second // must be ExternalWithdrawalPeriod > ExternalMessageLifetime and some time for balance update\n\tInternalWithdrawalPeriod  = 80 * time.Second\n\tExpirationProcessorPeriod = 5 * time.Second\n\n\tAllowableBlockchainLagging     = 40 * time.Second // TODO: use env var\n\tAllowableServiceToNodeTimeDiff = 2 * time.Second\n)\n\n// JettonProxyContractCode source code at https://github.com/gobicycle/ton-proxy-contract\nconst JettonProxyContractCode = \"B5EE9C72410102010037000114FF00F4A413F4BCF2C80B010050D33331D0D3030171B0915BE0FA4030ED44D0FA4030C705F2E1939320D74A97D4018100A0FB00E8301E8A9040\"\n\nconst MaxCommentLength = 1000 // qty in chars\n\nvar Config = struct {\n\tLiteServer               string `env:\"LITESERVER,required\"`\n\tLiteServerKey            string `env:\"LITESERVER_KEY,required\"`\n\tLiteServerRateLimit      int    `env:\"LITESERVER_RATE_LIMIT\" envDefault:\"100\"`\n\tSeed                     string `env:\"SEED,required\"`\n\tDatabaseURI              string `env:\"DB_URI,required\"`\n\tAPIPort                  int    `env:\"API_PORT,required\"`\n\tAPIToken                 string `env:\"API_TOKEN,required\"`\n\tTestnet                  bool   `env:\"IS_TESTNET\" envDefault:\"true\"`\n\tColdWalletString         string `env:\"COLD_WALLET\"`\n\tJettonString             string `env:\"JETTONS\"`\n\tTonString                string `env:\"TON_CUTOFFS,required\"`\n\tIsDepositSideCalculation bool   `env:\"DEPOSIT_SIDE_BALANCE\" envDefault:\"true\"` // TODO: rename to DEPOSIT_SIDE_CALCULATION\n\tQueueURI                 string `env:\"QUEUE_URI\"`\n\tQueueName                string `env:\"QUEUE_NAME\"`\n\tQueueEnabled             bool   `env:\"QUEUE_ENABLED\" envDefault:\"false\"`\n\tProofCheckEnabled        bool   `env:\"PROOF_CHECK_ENABLED\" envDefault:\"false\"`\n\tNetworkConfigUrl         string `env:\"NETWORK_CONFIG_URL\"`\n\tWebhookEndpoint          string `env:\"WEBHOOK_ENDPOINT\"`\n\tWebhookToken             string `env:\"WEBHOOK_TOKEN\"`\n\tAllowableLaggingSec      int    `env:\"ALLOWABLE_LAG\"`\n\tForwardTonAmount         int    `env:\"FORWARD_TON_AMOUNT\" envDefault:\"1\"`\n\tJettons                  map[string]Jetton\n\tTon                      Cutoffs\n\tColdWallet               *address.Address\n\tBlockchainConfig         *boc.Cell\n}{}\n\ntype Jetton struct {\n\tMaster             *address.Address\n\tWithdrawalCutoff   *big.Int\n\tHotWalletMaxCutoff *big.Int\n\tHotWalletResidual  *big.Int\n}\n\ntype Cutoffs struct {\n\tHotWalletMin      *big.Int\n\tHotWalletMax      *big.Int\n\tWithdrawal        *big.Int\n\tHotWalletResidual *big.Int\n}\n\nfunc GetConfig() {\n\terr := env.Parse(&Config)\n\tif err != nil {\n\t\tlog.Fatalf(\"Can not load config: %v\", err)\n\t}\n\tConfig.Jettons = parseJettonString(Config.JettonString)\n\tConfig.Ton = parseTonString(Config.TonString)\n\n\tif Config.ForwardTonAmount < 0 || Config.ForwardTonAmount > MaxJettonForwardTonAmount {\n\t\tlog.Fatalf(\"Forward TON amount for jetton transfer must be positive and less than %d\", MaxJettonForwardTonAmount)\n\t} else {\n\t\tJettonForwardAmount = tlb.FromNanoTONU(uint64(Config.ForwardTonAmount))\n\t}\n\n\tif Config.ColdWalletString != \"\" {\n\t\tcoldAddr, err := address.ParseAddr(Config.ColdWalletString)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Can not parse cold wallet address: %v\", err)\n\t\t}\n\t\tif coldAddr.Type() != address.StdAddress {\n\t\t\tlog.Fatalf(\"Only std cold wallet address supported\")\n\t\t}\n\t\tif coldAddr.IsTestnetOnly() && !Config.Testnet {\n\t\t\tlog.Fatalf(\"Can not use testnet cold wallet address for mainnet\")\n\t\t}\n\t\tConfig.ColdWallet = coldAddr\n\t}\n\n\tif Config.AllowableLaggingSec != 0 {\n\t\tAllowableBlockchainLagging = time.Second * time.Duration(Config.AllowableLaggingSec)\n\t}\n}\n\nfunc parseJettonString(s string) map[string]Jetton {\n\tres := make(map[string]Jetton)\n\tif s == \"\" {\n\t\treturn res\n\t}\n\tjettons := strings.Split(s, \",\")\n\tfor _, j := range jettons {\n\t\tdata := strings.Split(j, \":\")\n\t\tif len(data) != 4 && len(data) != 5 {\n\t\t\tlog.Fatalf(\"invalid jetton data\")\n\t\t}\n\t\tcur := data[0]\n\t\taddr, err := address.ParseAddr(data[1])\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"invalid jetton address: %v\", err)\n\t\t}\n\t\tmaxCutoff, err := decimal.NewFromString(data[2])\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"invalid %v jetton max cutoff: %v\", data[0], err)\n\t\t}\n\t\twithdrawalCutoff, err := decimal.NewFromString(data[3])\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"invalid %v jetton withdrawal cutoff: %v\", data[0], err)\n\t\t}\n\n\t\tresidual := maxCutoff.Mul(DefaultHotWalletHysteresis)\n\t\tif len(data) == 5 {\n\t\t\tresidual, err = decimal.NewFromString(data[4])\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"invalid hot_wallet_residual_balance parameter: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\tres[cur] = Jetton{\n\t\t\tMaster:             addr,\n\t\t\tWithdrawalCutoff:   withdrawalCutoff.BigInt(),\n\t\t\tHotWalletMaxCutoff: maxCutoff.BigInt(),\n\t\t\tHotWalletResidual:  residual.BigInt(),\n\t\t}\n\t}\n\treturn res\n}\n\nfunc parseTonString(s string) Cutoffs {\n\tdata := strings.Split(s, \":\")\n\tif len(data) != 3 && len(data) != 4 {\n\t\tlog.Fatalf(\"invalid TON cuttofs\")\n\t}\n\thotWalletMin, err := decimal.NewFromString(data[0])\n\tif err != nil {\n\t\tlog.Fatalf(\"invalid TON hot wallet min cutoff: %v\", err)\n\t}\n\thotWalletMax, err := decimal.NewFromString(data[1])\n\tif err != nil {\n\t\tlog.Fatalf(\"invalid TON hot wallet max cutoff: %v\", err)\n\t}\n\twithdrawal, err := decimal.NewFromString(data[2])\n\tif err != nil {\n\t\tlog.Fatalf(\"invalid TON withdrawal cutoff: %v\", err)\n\t}\n\tif hotWalletMin.Cmp(hotWalletMax) == 1 {\n\t\tlog.Fatalf(\"TON hot wallet max cutoff must be greater than TON hot wallet min cutoff\")\n\t}\n\n\tresidual := hotWalletMax.Mul(DefaultHotWalletHysteresis)\n\tif len(data) == 4 {\n\t\tresidual, err = decimal.NewFromString(data[3])\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"invalid hot_wallet_residual_balance parameter: %v\", err)\n\t\t}\n\t}\n\n\treturn Cutoffs{\n\t\tHotWalletMin:      hotWalletMin.BigInt(),\n\t\tHotWalletMax:      hotWalletMax.BigInt(),\n\t\tWithdrawal:        withdrawal.BigInt(),\n\t\tHotWalletResidual: residual.BigInt(),\n\t}\n}\n"
  },
  {
    "path": "core/block_scanner.go",
    "content": "package core\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gobicycle/bicycle/audit\"\n\t\"github.com/gobicycle/bicycle/config\"\n\t\"github.com/gofrs/uuid\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/tonkeeper/tongo\"\n\t\"github.com/tonkeeper/tongo/boc\"\n\ttongoTlb \"github.com/tonkeeper/tongo/tlb\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"github.com/xssnick/tonutils-go/tlb\"\n\t\"github.com/xssnick/tonutils-go/ton\"\n\t\"github.com/xssnick/tonutils-go/tvm/cell\"\n)\n\ntype BlockScanner struct {\n\tdb           storage\n\tblockchain   blockchain\n\tshard        byte\n\ttracker      blocksTracker\n\twg           *sync.WaitGroup\n\tnotificators []Notificator\n}\n\ntype transactions struct {\n\tAddress      Address\n\tWalletType   WalletType\n\tTransactions []*tlb.Transaction\n}\n\ntype jettonTransferNotificationMsg struct {\n\tAmount  Coins\n\tSender  *address.Address\n\tComment string\n}\n\ntype JettonTransferMsg struct {\n\tAmount      Coins\n\tDestination *address.Address\n\tComment     string\n}\n\ntype HighLoadWalletExtMsgInfo struct {\n\tUUID     uuid.UUID\n\tTTL      time.Time\n\tMessages *cell.Dictionary\n}\n\ntype incomeNotification struct {\n\tDeposit   string `json:\"deposit_address\"`\n\tTimestamp int64  `json:\"time\"`\n\tAmount    string `json:\"amount\"`\n\tSource    string `json:\"source_address,omitempty\"`\n\tComment   string `json:\"comment,omitempty\"`\n\tUserID    string `json:\"user_id\"`\n\tTxHash    string `json:\"tx_hash\"`\n}\n\nfunc NewBlockScanner(\n\twg *sync.WaitGroup,\n\tdb storage,\n\tblockchain blockchain,\n\tshard byte,\n\ttracker blocksTracker,\n\tnotificators []Notificator,\n) *BlockScanner {\n\tt := &BlockScanner{\n\t\tdb:           db,\n\t\tblockchain:   blockchain,\n\t\tshard:        shard,\n\t\ttracker:      tracker,\n\t\twg:           wg,\n\t\tnotificators: notificators,\n\t}\n\tt.wg.Add(1)\n\tgo t.Start()\n\treturn t\n}\n\nfunc (s *BlockScanner) Start() {\n\tdefer s.wg.Done()\n\tlog.Printf(\"Block scanner started\")\n\tfor {\n\t\tblock, exit, err := s.tracker.NextBlock()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"get block error: %v\", err)\n\t\t}\n\t\tif exit {\n\t\t\tlog.Printf(\"Block scanner stopped\")\n\t\t\tbreak\n\t\t}\n\t\tctx, cancel := context.WithTimeout(context.Background(), time.Second*15)\n\t\terr = s.processBlock(ctx, block)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"block processing error: %v\", err)\n\t\t}\n\t\tcancel()\n\t}\n}\n\nfunc (s *BlockScanner) Stop() {\n\ts.tracker.Stop()\n}\n\nfunc (s *BlockScanner) processBlock(ctx context.Context, block ShardBlockHeader) error {\n\ttxIDs, err := s.blockchain.GetTransactionIDsFromBlock(ctx, block.BlockIDExt)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfilteredTXs, err := s.filterTXs(ctx, block.BlockIDExt, txIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\te, err := s.processTXs(ctx, filteredTXs, block)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = s.db.SaveParsedBlockData(ctx, e)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// Push notifications after saving to the database.\n\t// Prevents duplicate sending on restart, but may result in lost notifications.\n\treturn s.pushNotifications(e)\n}\n\nfunc (s *BlockScanner) pushNotifications(e BlockEvents) error {\n\tif len(s.notificators) == 0 {\n\t\treturn nil\n\t}\n\n\tif config.Config.IsDepositSideCalculation {\n\t\tfor _, ei := range e.ExternalIncomes {\n\t\t\terr := s.pushNotification(ei.To, ei.Amount, ei.Utime, ei.From, ei.FromWorkchain, ei.Comment, ei.TxHash)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor _, ii := range e.InternalIncomes {\n\t\t\terr := s.pushNotification(ii.From, ii.Amount, ii.Utime, nil, nil, \"\", ii.TxHash)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *BlockScanner) pushNotification(\n\taddr Address,\n\tamount Coins,\n\ttimestamp uint32,\n\tfrom []byte,\n\tfromWorkchain *int32,\n\tcomment string,\n\ttxHash []byte,\n) error {\n\towner := s.db.GetOwner(addr)\n\tif owner != nil {\n\t\taddr = *owner\n\t}\n\tuserID, ok := s.db.GetUserID(addr)\n\tif !ok {\n\t\treturn fmt.Errorf(\"not found UserID for deposit %s\", addr.ToUserFormat())\n\t}\n\tnotification := incomeNotification{\n\t\tDeposit:   addr.ToUserFormat(),\n\t\tAmount:    amount.String(),\n\t\tTimestamp: int64(timestamp),\n\t\tComment:   comment,\n\t\tUserID:    userID,\n\t\tTxHash:    fmt.Sprintf(\"%x\", txHash),\n\t}\n\tif len(from) == 32 && fromWorkchain != nil {\n\t\t// supports only std address\n\t\tsrc := address.NewAddress(0, byte(*fromWorkchain), from)\n\t\tsrc.SetTestnetOnly(config.Config.Testnet)\n\t\tnotification.Source = src.String()\n\t}\n\n\tfor _, n := range s.notificators {\n\t\terr := n.Publish(notification)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *BlockScanner) filterTXs(\n\tctx context.Context,\n\tblockID *ton.BlockIDExt,\n\tids []ton.TransactionShortInfo,\n) (\n\t[]transactions, error,\n) {\n\ttxMap := make(map[Address][]*tlb.Transaction)\n\tfor _, id := range ids {\n\t\ta, err := AddressFromBytes(id.Account) // must be int256 for lite api\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t_, ok := s.db.GetWalletType(a)\n\t\tif ok {\n\t\t\ttx, err := s.blockchain.GetTransactionFromBlock(ctx, blockID, id)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\ttxMap[a] = append(txMap[a], tx)\n\t\t}\n\t}\n\tvar res []transactions\n\tfor a, txs := range txMap {\n\t\twType, _ := s.db.GetWalletType(a)\n\t\tres = append(res, transactions{a, wType, txs})\n\t}\n\treturn res, nil\n}\n\nfunc checkTxForSuccess(tx *tlb.Transaction) (bool, error) {\n\tcell1, err := tlb.ToCell(tx.Description)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tc, err := boc.DeserializeBoc(cell1.ToBOC())\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tvar desc tongoTlb.TransactionDescr\n\terr = tongoTlb.Unmarshal(c[0], &desc)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tvar fakeTx tongo.Transaction // need for check tx success via tongo\n\tfakeTx.Description = desc\n\treturn fakeTx.IsSuccess(), nil\n}\n\nfunc (s *BlockScanner) processTXs(\n\tctx context.Context,\n\ttxs []transactions,\n\tblock ShardBlockHeader,\n) (\n\tBlockEvents, error,\n) {\n\tblockEvents := BlockEvents{Block: block}\n\tfor _, t := range txs {\n\t\tswitch t.WalletType {\n\t\t// TODO: check order of Lt for different accounts (it is important for intermediate tx Lt)\n\t\tcase TonHotWallet:\n\t\t\thotWalletEvents, err := s.processTonHotWalletTXs(t)\n\t\t\tif err != nil {\n\t\t\t\treturn BlockEvents{}, err\n\t\t\t}\n\t\t\tblockEvents.Append(hotWalletEvents)\n\t\tcase TonDepositWallet:\n\t\t\ttonDepositEvents, err := s.processTonDepositWalletTXs(t)\n\t\t\tif err != nil {\n\t\t\t\treturn BlockEvents{}, err\n\t\t\t}\n\t\t\tblockEvents.Append(tonDepositEvents)\n\t\tcase JettonDepositWallet:\n\t\t\tjettonDepositEvents, err := s.processJettonDepositWalletTXs(ctx, t, block.BlockIDExt, block.Parent)\n\t\t\tif err != nil {\n\t\t\t\treturn BlockEvents{}, err\n\t\t\t}\n\t\t\tblockEvents.Append(jettonDepositEvents)\n\t\t}\n\t}\n\treturn blockEvents, nil\n}\n\nfunc (s *BlockScanner) processTonHotWalletTXs(txs transactions) (Events, error) {\n\tvar events Events\n\n\tfor _, tx := range txs.Transactions {\n\n\t\tif tx.IO.In == nil { // impossible for standard highload TON wallet\n\t\t\taudit.LogTX(audit.Error, string(TonHotWallet), tx.Hash, \"transaction without in message\")\n\t\t\treturn Events{}, fmt.Errorf(\"anomalous behavior of the TON hot wallet\")\n\t\t}\n\n\t\tswitch tx.IO.In.MsgType {\n\t\tcase tlb.MsgTypeExternalIn:\n\t\t\te, err := s.processTonHotWalletExternalInMsg(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn Events{}, err\n\t\t\t}\n\t\t\tevents.Append(e)\n\t\tcase tlb.MsgTypeInternal:\n\t\t\te, err := s.processTonHotWalletInternalInMsg(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn Events{}, err\n\t\t\t}\n\t\t\tevents.Append(e)\n\t\tdefault:\n\t\t\taudit.LogTX(audit.Error, string(TonHotWallet), tx.Hash,\n\t\t\t\t\"transaction in message must be internal or external in\")\n\t\t\treturn Events{}, fmt.Errorf(\"anomalous behavior of the TON hot wallet\")\n\t\t}\n\t}\n\treturn events, nil\n}\n\nfunc (s *BlockScanner) processTonDepositWalletTXs(txs transactions) (Events, error) {\n\tvar events Events\n\n\tfor _, tx := range txs.Transactions {\n\n\t\tif tx.IO.In == nil { // impossible for standard TON V3 wallet\n\t\t\taudit.LogTX(audit.Error, string(TonDepositWallet), tx.Hash, \"transaction without in message\")\n\t\t\treturn Events{}, fmt.Errorf(\"anomalous behavior of the deposit TON wallet\")\n\t\t}\n\n\t\tswitch tx.IO.In.MsgType {\n\t\tcase tlb.MsgTypeExternalIn:\n\t\t\t// internal withdrawal. spam or invalid external cannot invoke tx\n\t\t\t// theoretically will be up to 4 out messages for TON V3 wallet\n\t\t\t// external_in msg without out_msg very rare or impossible\n\t\t\t// it is not critical for internal transfers (double spending not dangerous).\n\t\t\tsuccess, err := checkTxForSuccess(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn Events{}, err\n\t\t\t}\n\t\t\tif !success {\n\t\t\t\taudit.LogTX(audit.Info, string(TonDepositWallet), tx.Hash, \"failed transaction\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\te, err := s.processTonDepositWalletExternalInMsg(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn Events{}, err\n\t\t\t}\n\t\t\tevents.Append(e)\n\t\tcase tlb.MsgTypeInternal:\n\t\t\t// external payment income\n\t\t\t// internal message can not invoke out message for TON wallet V3 except of bounce\n\t\t\t// bounced filtered by len(tx.IO.Out) != 0\n\t\t\tif tx.OutMsgCount != 0 {\n\t\t\t\taudit.LogTX(audit.Info, string(TonDepositWallet), tx.Hash, \"ton deposit filling is bounced\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\te, err := s.processTonDepositWalletInternalInMsg(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn Events{}, err\n\t\t\t}\n\t\t\tevents.Append(e)\n\t\tdefault:\n\t\t\taudit.LogTX(audit.Error, string(TonDepositWallet), tx.Hash,\n\t\t\t\t\"transaction in message must be internal or external in\")\n\t\t\treturn Events{}, fmt.Errorf(\"anomalous behavior of the deposit TON wallet\")\n\t\t}\n\t}\n\treturn events, nil\n}\n\nfunc (s *BlockScanner) processJettonDepositWalletTXs(\n\tctx context.Context,\n\ttxs transactions,\n\tblockID, prevBlockID *ton.BlockIDExt,\n) (Events, error) {\n\tvar (\n\t\tunknownTransactions []*tlb.Transaction\n\t\tevents              Events\n\t)\n\n\tknownIncomeAmount := big.NewInt(0)\n\ttotalWithdrawalsAmount := big.NewInt(0)\n\n\tfor _, tx := range txs.Transactions {\n\t\te, knownAmount, outUnknownFound, err := s.processJettonDepositOutMsgs(tx)\n\t\tif err != nil {\n\t\t\treturn Events{}, err\n\t\t}\n\t\tknownIncomeAmount.Add(knownIncomeAmount, knownAmount)\n\t\tevents.Append(e)\n\n\t\te, totalAmount, inUnknownFound, err := s.processJettonDepositInMsg(tx)\n\t\tif err != nil {\n\t\t\treturn Events{}, err\n\t\t}\n\t\ttotalWithdrawalsAmount.Add(totalWithdrawalsAmount, totalAmount)\n\t\tevents.Append(e)\n\n\t\tif outUnknownFound || inUnknownFound { // if found some unknown messages that potentially can change Jetton balance\n\t\t\tunknownTransactions = append(unknownTransactions, tx)\n\t\t}\n\t}\n\n\tunknownIncomeAmount, err := s.calculateJettonAmounts(ctx, txs.Address, prevBlockID, blockID, knownIncomeAmount, totalWithdrawalsAmount)\n\tif err != nil {\n\t\treturn Events{}, err\n\t}\n\n\tif unknownIncomeAmount.Cmp(big.NewInt(0)) == 1 { // unknownIncomeAmount > 0\n\t\tunknownIncomes, err := convertUnknownJettonTxs(unknownTransactions, txs.Address, unknownIncomeAmount)\n\t\tif err != nil {\n\t\t\treturn Events{}, err\n\t\t}\n\t\tevents.ExternalIncomes = append(events.ExternalIncomes, unknownIncomes...)\n\t}\n\n\treturn events, nil\n}\n\nfunc (s *BlockScanner) calculateJettonAmounts(\n\tctx context.Context,\n\taddress Address,\n\tprevBlockID, blockID *ton.BlockIDExt,\n\tknownIncomeAmount, totalWithdrawalsAmount *big.Int,\n) (\n\tunknownIncomeAmount *big.Int,\n\terr error,\n) {\n\tprevBalance, err := s.blockchain.GetJettonBalance(ctx, address, prevBlockID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcurrentBalance, err := s.blockchain.GetJettonBalance(ctx, address, blockID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdiff := big.NewInt(0)\n\tdiff.Sub(currentBalance, prevBalance) // diff = currentBalance - prevBalance\n\n\ttotalIncomeAmount := big.NewInt(0)\n\ttotalIncomeAmount.Add(diff, totalWithdrawalsAmount) // totalIncomeAmount = diff + totalWithdrawalsAmount\n\n\tunknownIncomeAmount = big.NewInt(0)\n\tunknownIncomeAmount.Sub(totalIncomeAmount, knownIncomeAmount) // unknownIncomeAmount = totalIncomeAmount - knownIncomeAmount\n\n\treturn unknownIncomeAmount, nil\n}\n\nfunc convertUnknownJettonTxs(txs []*tlb.Transaction, addr Address, amount *big.Int) ([]ExternalIncome, error) {\n\tvar incomes []ExternalIncome\n\tfor _, tx := range txs { // unknown sender (jetton wallet owner). do not save message sender as from.\n\t\tincomes = append(incomes, ExternalIncome{\n\t\t\tUtime:  tx.Now,\n\t\t\tLt:     tx.LT,\n\t\t\tTo:     addr,\n\t\t\tAmount: ZeroCoins(),\n\t\t\tTxHash: tx.Hash,\n\t\t})\n\n\t}\n\tif len(txs) > 0 {\n\t\tincomes = append(incomes, ExternalIncome{\n\t\t\tUtime:  txs[0].Now, // mark unknown tx with first tx time\n\t\t\tLt:     txs[0].LT,\n\t\t\tTo:     addr,\n\t\t\tAmount: NewCoins(amount),\n\t\t\tTxHash: txs[0].Hash,\n\t\t})\n\t}\n\treturn incomes, nil\n}\n\nfunc decodeJettonTransferNotification(msg *tlb.InternalMessage) (jettonTransferNotificationMsg, error) {\n\tif msg == nil {\n\t\treturn jettonTransferNotificationMsg{}, fmt.Errorf(\"nil msg\")\n\t}\n\tpayload := msg.Payload()\n\tif payload == nil {\n\t\treturn jettonTransferNotificationMsg{}, fmt.Errorf(\"empty payload\")\n\t}\n\tvar notification struct {\n\t\t_              tlb.Magic        `tlb:\"#7362d09c\"`\n\t\tQueryID        uint64           `tlb:\"## 64\"`\n\t\tAmount         tlb.Coins        `tlb:\".\"`\n\t\tSender         *address.Address `tlb:\"addr\"`\n\t\tForwardPayload *cell.Cell       `tlb:\"either . ^\"`\n\t}\n\terr := tlb.LoadFromCell(&notification, payload.BeginParse())\n\tif err != nil {\n\t\treturn jettonTransferNotificationMsg{}, err\n\t}\n\treturn jettonTransferNotificationMsg{\n\t\tSender:  notification.Sender,\n\t\tAmount:  NewCoins(notification.Amount.Nano()),\n\t\tComment: LoadComment(notification.ForwardPayload),\n\t}, nil\n}\n\nfunc DecodeJettonTransfer(msg *tlb.InternalMessage) (JettonTransferMsg, error) {\n\tif msg == nil {\n\t\treturn JettonTransferMsg{}, fmt.Errorf(\"nil msg\")\n\t}\n\tpayload := msg.Payload()\n\tif payload == nil {\n\t\treturn JettonTransferMsg{}, fmt.Errorf(\"empty payload\")\n\t}\n\tvar transfer struct {\n\t\t_                   tlb.Magic        `tlb:\"#0f8a7ea5\"`\n\t\tQueryID             uint64           `tlb:\"## 64\"`\n\t\tAmount              tlb.Coins        `tlb:\".\"`\n\t\tDestination         *address.Address `tlb:\"addr\"`\n\t\tResponseDestination *address.Address `tlb:\"addr\"`\n\t\tCustomPayload       *cell.Cell       `tlb:\"maybe ^\"`\n\t\tForwardTonAmount    tlb.Coins        `tlb:\".\"`\n\t\tForwardPayload      *cell.Cell       `tlb:\"either . ^\"`\n\t}\n\terr := tlb.LoadFromCell(&transfer, payload.BeginParse())\n\tif err != nil {\n\t\treturn JettonTransferMsg{}, err\n\t}\n\treturn JettonTransferMsg{\n\t\tNewCoins(transfer.Amount.Nano()),\n\t\ttransfer.Destination,\n\t\tLoadComment(transfer.ForwardPayload),\n\t}, nil\n}\n\nfunc decodeJettonExcesses(msg *tlb.InternalMessage) (int64, error) {\n\tif msg == nil {\n\t\treturn 0, fmt.Errorf(\"nil msg\")\n\t}\n\tpayload := msg.Payload()\n\tif payload == nil {\n\t\treturn 0, fmt.Errorf(\"empty payload\")\n\t}\n\tvar excesses struct {\n\t\t_       tlb.Magic `tlb:\"#d53276db\"`\n\t\tQueryID uint64    `tlb:\"## 64\"`\n\t}\n\terr := tlb.LoadFromCell(&excesses, payload.BeginParse())\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn int64(excesses.QueryID), nil\n}\n\nfunc parseExternalMessage(msg *tlb.ExternalMessage) (\n\tu uuid.UUID,\n\taddrMap map[Address]struct{},\n\tisValidWithdrawal bool,\n\terr error,\n) {\n\tif msg == nil {\n\t\treturn uuid.UUID{}, nil, false, fmt.Errorf(\"nil msg\")\n\t}\n\taddrMap = make(map[Address]struct{})\n\n\tinfo, err := getHighLoadWalletExtMsgInfo(msg)\n\tif err != nil {\n\t\treturn uuid.UUID{}, nil, false, err\n\t}\n\n\tfor _, m := range info.Messages.All() {\n\t\tvar (\n\t\t\tintMsg tlb.InternalMessage\n\t\t\taddr   Address\n\t\t)\n\t\tmsgCell, err := m.Value.BeginParse().LoadRef()\n\t\tif err != nil {\n\t\t\treturn uuid.UUID{}, nil, false, err\n\t\t}\n\t\terr = tlb.LoadFromCell(&intMsg, msgCell)\n\t\tif err != nil {\n\t\t\treturn uuid.UUID{}, nil, false, err\n\t\t}\n\t\tjettonTransfer, err := DecodeJettonTransfer(&intMsg)\n\t\tif err == nil {\n\t\t\taddr, err = AddressFromTonutilsAddress(jettonTransfer.Destination)\n\t\t\tif err != nil {\n\t\t\t\treturn uuid.UUID{}, nil, false, nil\n\t\t\t}\n\t\t} else {\n\t\t\taddr, err = AddressFromTonutilsAddress(intMsg.DstAddr)\n\t\t\tif err != nil {\n\t\t\t\treturn uuid.UUID{}, nil, false, nil\n\t\t\t}\n\t\t}\n\t\t_, ok := addrMap[addr]\n\t\tif ok { // not unique addresses\n\t\t\treturn uuid.UUID{}, nil, false, nil\n\t\t}\n\t\taddrMap[addr] = struct{}{}\n\t}\n\treturn info.UUID, addrMap, true, nil\n}\n\nfunc (s *BlockScanner) failedWithdrawals(inMap map[Address]struct{}, outMap map[Address]struct{}, u uuid.UUID, txHash []byte) []ExternalWithdrawal {\n\tvar w []ExternalWithdrawal\n\tfor i := range inMap {\n\t\t_, dstOk := s.db.GetWalletType(i)\n\t\tif _, ok := outMap[i]; !ok && !dstOk { // !dstOk - not failed internal fee payments\n\t\t\tw = append(w, ExternalWithdrawal{ExtMsgUuid: u, To: i, IsFailed: true, TxHash: txHash})\n\t\t\taudit.LogTX(audit.Error, string(TonHotWallet), txHash, fmt.Sprintf(\"Failed external withdrawal to %v\", i.ToUserFormat()))\n\t\t} else if !ok && dstOk { // failed internal fee payments\n\t\t\t// TODO: cause a fatal error or increment error counter\n\t\t\taudit.LogTX(audit.Error, string(TonHotWallet), txHash, fmt.Sprintf(\"Failed internal withdrawal to %v\", i.ToUserFormat()))\n\t\t}\n\t}\n\treturn w\n}\n\nfunc getHighLoadWalletExtMsgInfo(extMsg *tlb.ExternalMessage) (HighLoadWalletExtMsgInfo, error) {\n\tbody := extMsg.Payload()\n\tif body == nil {\n\t\treturn HighLoadWalletExtMsgInfo{}, fmt.Errorf(\"nil body for external message\")\n\t}\n\thash := body.Hash() // must be 32 bytes\n\tu, err := uuid.FromBytes(hash[:16])\n\tif err != nil {\n\t\treturn HighLoadWalletExtMsgInfo{}, err\n\t}\n\n\tvar data struct {\n\t\tSign        []byte           `tlb:\"bits 512\"`\n\t\tSubwalletID uint32           `tlb:\"## 32\"`\n\t\tBoundedID   uint64           `tlb:\"## 64\"`\n\t\tMessages    *cell.Dictionary `tlb:\"dict 16\"`\n\t}\n\terr = tlb.LoadFromCell(&data, body.BeginParse())\n\tif err != nil {\n\t\treturn HighLoadWalletExtMsgInfo{}, err\n\t}\n\tttl := time.Unix(int64((data.BoundedID>>32)&0x00_00_00_00_FF_FF_FF_FF), 0)\n\treturn HighLoadWalletExtMsgInfo{UUID: u, TTL: ttl, Messages: data.Messages}, nil\n}\n\nfunc (s *BlockScanner) processTonHotWalletExternalInMsg(tx *tlb.Transaction) (Events, error) {\n\tvar events Events\n\tinMsg := tx.IO.In.AsExternalIn()\n\t// withdrawal messages must be only with different recipients for identification\n\tu, addrMapIn, isValid, err := parseExternalMessage(inMsg)\n\tif err != nil {\n\t\treturn Events{}, err\n\t}\n\tif !isValid {\n\t\taudit.LogTX(audit.Error, string(TonHotWallet), tx.Hash, \"not valid external message\")\n\t\treturn Events{}, fmt.Errorf(\"not valid message\")\n\t}\n\n\taddrMapOut := make(map[Address]struct{})\n\n\tvar outList []tlb.Message\n\n\tif tx.OutMsgCount > 0 {\n\t\toutList, err = tx.IO.Out.ToSlice()\n\t\tif err != nil {\n\t\t\treturn Events{}, err\n\t\t}\n\t}\n\n\tfor _, m := range outList {\n\t\tif m.MsgType != tlb.MsgTypeInternal {\n\t\t\taudit.LogTX(audit.Error, string(TonHotWallet), tx.Hash, \"not internal out message for transaction\")\n\t\t\treturn Events{}, fmt.Errorf(\"anomalous behavior of the TON hot wallet\")\n\t\t}\n\t\tmsg := m.AsInternal()\n\n\t\taddr, err := AddressFromTonutilsAddress(msg.DstAddr)\n\t\tif err != nil {\n\t\t\treturn Events{}, fmt.Errorf(\"invalid address in withdrawal message\")\n\t\t}\n\t\tdstType, dstOk := s.db.GetWalletTypeByTonutilsAddress(msg.DstAddr)\n\n\t\tif dstOk && dstType == JettonHotWallet { // Jetton external withdrawal\n\t\t\tjettonTransfer, err := DecodeJettonTransfer(msg)\n\t\t\tif err != nil {\n\t\t\t\taudit.LogTX(audit.Error, string(TonHotWallet), tx.Hash, \"invalid jetton transfer message to hot jetton wallet\")\n\t\t\t\treturn Events{}, fmt.Errorf(\"invalid jetton transfer message to hot jetton wallet\")\n\t\t\t}\n\t\t\ta, err := AddressFromTonutilsAddress(jettonTransfer.Destination)\n\t\t\tif err != nil {\n\t\t\t\treturn Events{}, fmt.Errorf(\"invalid address in withdrawal message\")\n\t\t\t}\n\t\t\tevents.ExternalWithdrawals = append(events.ExternalWithdrawals, ExternalWithdrawal{\n\t\t\t\tExtMsgUuid: u,\n\t\t\t\tUtime:      msg.CreatedAt,\n\t\t\t\tLt:         msg.CreatedLT,\n\t\t\t\tTo:         a,\n\t\t\t\tAmount:     jettonTransfer.Amount,\n\t\t\t\tComment:    jettonTransfer.Comment,\n\t\t\t\tIsFailed:   false,\n\t\t\t\tTxHash:     tx.Hash,\n\t\t\t})\n\t\t\taddrMapOut[a] = struct{}{}\n\t\t\tcontinue\n\t\t}\n\n\t\tif dstOk && dstType == JettonOwner { // Jetton internal withdrawal or service withdrawal\n\t\t\te, err := s.processTonHotWalletProxyMsg(msg)\n\t\t\tif err != nil {\n\t\t\t\treturn Events{}, fmt.Errorf(\"jetton withdrawal error: %v\", err)\n\t\t\t}\n\t\t\tevents.Append(e)\n\t\t\taddrMapOut[addr] = struct{}{}\n\t\t\tcontinue\n\t\t}\n\n\t\tif !dstOk { // hot_wallet -> unknown_address. to filter internal fee payments\n\t\t\tevents.ExternalWithdrawals = append(events.ExternalWithdrawals, ExternalWithdrawal{\n\t\t\t\tExtMsgUuid: u,\n\t\t\t\tUtime:      msg.CreatedAt,\n\t\t\t\tLt:         msg.CreatedLT,\n\t\t\t\tTo:         addr,\n\t\t\t\tAmount:     NewCoins(msg.Amount.Nano()),\n\t\t\t\tComment:    msg.Comment(),\n\t\t\t\tIsFailed:   false,\n\t\t\t\tTxHash:     tx.Hash,\n\t\t\t})\n\t\t}\n\t\taddrMapOut[addr] = struct{}{}\n\t}\n\tevents.ExternalWithdrawals = append(events.ExternalWithdrawals, s.failedWithdrawals(addrMapIn, addrMapOut, u, tx.Hash)...)\n\treturn events, nil\n}\n\nfunc (s *BlockScanner) processTonHotWalletProxyMsg(msg *tlb.InternalMessage) (Events, error) {\n\tvar events Events\n\tbody := msg.Payload()\n\tinternalPayload, err := body.BeginParse().LoadRef()\n\tif err != nil {\n\t\treturn Events{}, fmt.Errorf(\"no internal payload to proxy contract: %v\", err)\n\t}\n\tvar intMsg tlb.InternalMessage\n\terr = tlb.LoadFromCell(&intMsg, internalPayload)\n\tif err != nil {\n\t\treturn Events{}, fmt.Errorf(\"can not decode payload message for proxy contract: %v\", err)\n\t}\n\n\tdestType, ok := s.db.GetWalletTypeByTonutilsAddress(intMsg.DstAddr)\n\t// ok && destType == TonHotWallet - service TON withdrawal\n\t// !ok - service Jetton withdrawal\n\tif ok && destType == JettonDepositWallet { // Jetton internal withdrawal\n\t\tjettonTransfer, err := DecodeJettonTransfer(&intMsg)\n\t\tif err != nil {\n\t\t\treturn Events{}, fmt.Errorf(\"invalid jetton transfer message to deposit jetton wallet: %v\", err)\n\t\t}\n\t\ta, err := AddressFromTonutilsAddress(jettonTransfer.Destination)\n\t\tif err != nil {\n\t\t\treturn Events{}, fmt.Errorf(\"invalid address in withdrawal message\")\n\t\t}\n\t\tevents.SendingConfirmations = append(events.SendingConfirmations, SendingConfirmation{\n\t\t\tLt:   msg.CreatedLT,\n\t\t\tFrom: a,\n\t\t\tMemo: jettonTransfer.Comment,\n\t\t})\n\t}\n\treturn events, nil\n}\n\nfunc (s *BlockScanner) processTonHotWalletInternalInMsg(tx *tlb.Transaction) (Events, error) {\n\tvar events Events\n\tinMsg := tx.IO.In.AsInternal()\n\tsrcAddr, err := AddressFromTonutilsAddress(inMsg.SrcAddr)\n\tif err != nil {\n\t\treturn Events{}, err\n\t}\n\tdstAddr, err := AddressFromTonutilsAddress(inMsg.DstAddr)\n\tif err != nil {\n\t\treturn Events{}, err\n\t}\n\n\tsrcType, srcOk := s.db.GetWalletType(srcAddr)\n\tif !srcOk { // unknown_address -> hot_wallet. to check for external jetton transfer confirmation via excess message\n\t\tqueryID, err := decodeJettonExcesses(inMsg)\n\t\tif err == nil {\n\t\t\tevents.WithdrawalConfirmations = append(events.WithdrawalConfirmations,\n\t\t\t\tJettonWithdrawalConfirmation{queryID})\n\t\t}\n\t} else if srcOk && srcType == TonDepositWallet { // income TONs from deposit\n\t\tincome := InternalIncome{\n\t\t\tLt:       inMsg.CreatedLT,\n\t\t\tUtime:    inMsg.CreatedAt,\n\t\t\tFrom:     srcAddr,\n\t\t\tTo:       dstAddr,\n\t\t\tAmount:   NewCoins(inMsg.Amount.Nano()),\n\t\t\tMemo:     inMsg.Comment(),\n\t\t\tIsFailed: false,\n\t\t\tTxHash:   tx.Hash,\n\t\t}\n\t\tsuccess, err := checkTxForSuccess(tx)\n\t\tif err != nil {\n\t\t\treturn Events{}, err\n\t\t}\n\t\t// TODO: check for partially failed message\n\t\tif success {\n\t\t\tevents.InternalIncomes = append(events.InternalIncomes, income)\n\t\t} else {\n\t\t\tincome.IsFailed = true\n\t\t\tevents.InternalIncomes = append(events.InternalIncomes, income)\n\t\t}\n\t} else if srcOk && srcType == JettonHotWallet { // income Jettons notification from Jetton hot wallet\n\t\tincome, err := decodeJettonTransferNotification(inMsg)\n\t\tif err == nil {\n\t\t\tsender, err := AddressFromTonutilsAddress(income.Sender)\n\t\t\tif err != nil {\n\t\t\t\treturn Events{}, err\n\t\t\t}\n\t\t\tfromType, fromOk := s.db.GetWalletType(sender)\n\t\t\tif !fromOk || fromType != JettonOwner { // skip transfers not from deposit wallets\n\t\t\t\treturn events, nil\n\t\t\t}\n\t\t\tevents.InternalIncomes = append(events.InternalIncomes, InternalIncome{\n\t\t\t\tLt:       inMsg.CreatedLT,\n\t\t\t\tUtime:    inMsg.CreatedAt,\n\t\t\t\tFrom:     sender, // sender == owner of jetton deposit wallet\n\t\t\t\tTo:       srcAddr,\n\t\t\t\tAmount:   income.Amount,\n\t\t\t\tMemo:     income.Comment,\n\t\t\t\tIsFailed: false,\n\t\t\t\tTxHash:   tx.Hash,\n\t\t\t})\n\t\t}\n\t}\n\treturn events, nil\n}\n\nfunc (s *BlockScanner) processTonDepositWalletExternalInMsg(tx *tlb.Transaction) (Events, error) {\n\tvar events Events\n\n\tdstAddr, err := AddressFromTonutilsAddress(tx.IO.In.AsExternalIn().DstAddr)\n\tif err != nil {\n\t\treturn Events{}, err\n\t}\n\n\tvar outList []tlb.Message\n\n\tif tx.OutMsgCount > 0 {\n\t\toutList, err = tx.IO.Out.ToSlice()\n\t\tif err != nil {\n\t\t\treturn Events{}, err\n\t\t}\n\t}\n\n\tfor _, o := range outList {\n\t\tif o.MsgType != tlb.MsgTypeInternal {\n\t\t\taudit.LogTX(audit.Error, string(TonDepositWallet), tx.Hash, \"not internal out message for transaction\")\n\t\t\treturn Events{}, fmt.Errorf(\"anomalous behavior of the deposit TON wallet\")\n\t\t}\n\t\tmsg := o.AsInternal()\n\t\tt, srcOk := s.db.GetWalletTypeByTonutilsAddress(msg.DstAddr)\n\t\tif !srcOk || t != TonHotWallet {\n\t\t\taudit.LogTX(audit.Warning, string(TonDepositWallet), tx.Hash, fmt.Sprintf(\"TONs withdrawal from %v to %v (not to hot wallet)\",\n\t\t\t\tmsg.SrcAddr.String(), msg.DstAddr.String()))\n\t\t\tcontinue\n\t\t}\n\t\tevents.SendingConfirmations = append(events.SendingConfirmations, SendingConfirmation{\n\t\t\tLt:   msg.CreatedLT,\n\t\t\tFrom: dstAddr,\n\t\t\tMemo: msg.Comment(),\n\t\t})\n\t\tevents.InternalWithdrawals = append(events.InternalWithdrawals, InternalWithdrawal{\n\t\t\tUtime:    msg.CreatedAt,\n\t\t\tLt:       msg.CreatedLT,\n\t\t\tFrom:     dstAddr,\n\t\t\tAmount:   NewCoins(msg.Amount.Nano()),\n\t\t\tMemo:     msg.Comment(),\n\t\t\tIsFailed: false,\n\t\t})\n\t}\n\treturn events, nil\n}\n\nfunc (s *BlockScanner) processTonDepositWalletInternalInMsg(tx *tlb.Transaction) (Events, error) {\n\tvar (\n\t\tevents        Events\n\t\tfrom          Address\n\t\terr           error\n\t\tfromWorkchain *int32\n\t)\n\n\tinMsg := tx.IO.In.AsInternal()\n\tdstAddr, err := AddressFromTonutilsAddress(inMsg.DstAddr)\n\tif err != nil {\n\t\treturn Events{}, err\n\t}\n\n\tisKnownSender := false\n\t// support only std address\n\tif inMsg.SrcAddr.Type() == address.StdAddress {\n\t\tfrom, err = AddressFromTonutilsAddress(inMsg.SrcAddr)\n\t\tif err != nil {\n\t\t\treturn Events{}, err\n\t\t}\n\t\t_, isKnownSender = s.db.GetWalletType(from)\n\t\twc := inMsg.SrcAddr.Workchain()\n\t\tfromWorkchain = &wc\n\t}\n\tif !isKnownSender { // income TONs from payer. exclude internal (hot->deposit, deposit->deposit) transfers.\n\t\tevents.ExternalIncomes = append(events.ExternalIncomes, ExternalIncome{\n\t\t\tLt:            tx.LT,\n\t\t\tUtime:         tx.Now,\n\t\t\tFrom:          from.ToBytes(),\n\t\t\tFromWorkchain: fromWorkchain,\n\t\t\tTo:            dstAddr,\n\t\t\tAmount:        NewCoins(inMsg.Amount.Nano()),\n\t\t\tComment:       inMsg.Comment(),\n\t\t\tTxHash:        tx.Hash,\n\t\t})\n\t}\n\treturn events, nil\n}\n\nfunc (s *BlockScanner) processJettonDepositOutMsgs(tx *tlb.Transaction) (Events, *big.Int, bool, error) {\n\tvar events Events\n\tknownIncomeAmount := big.NewInt(0)\n\tunknownMsgFound := false\n\n\tvar (\n\t\toutList []tlb.Message\n\t\terr     error\n\t)\n\n\tif tx.OutMsgCount > 0 {\n\t\toutList, err = tx.IO.Out.ToSlice()\n\t\tif err != nil {\n\t\t\treturn Events{}, nil, false, err\n\t\t}\n\t}\n\n\tfor _, m := range outList { // checks for JettonTransferNotification\n\n\t\tif m.MsgType != tlb.MsgTypeInternal {\n\t\t\taudit.LogTX(audit.Info, string(JettonDepositWallet), tx.Hash, \"sends external out message\")\n\t\t\tunknownMsgFound = true\n\t\t\tcontinue\n\t\t} // skip external_out msg\n\n\t\toutMsg := m.AsInternal()\n\t\tsrcAddr, err := AddressFromTonutilsAddress(outMsg.SrcAddr)\n\t\tif err != nil {\n\t\t\treturn Events{}, nil, false, err\n\t\t}\n\n\t\tnotify, err := decodeJettonTransferNotification(outMsg)\n\t\tif err != nil {\n\t\t\tunknownMsgFound = true\n\t\t\tcontinue\n\t\t}\n\n\t\t// need not check success. impossible for failed txs.\n\t\t_, senderOk := s.db.GetWalletTypeByTonutilsAddress(notify.Sender)\n\t\tif senderOk {\n\t\t\t// TODO: check balance calculation for unknown transactions for service transfers\n\t\t\taudit.LogTX(audit.Info, string(JettonDepositWallet), tx.Hash, \"service Jetton transfer\")\n\t\t\t// not set unknownMsgFound = true to prevent service transfers interpretation as unknown\n\t\t\tcontinue\n\t\t} // some kind of internal transfer\n\n\t\tdstAddr, err := AddressFromTonutilsAddress(outMsg.DstAddr)\n\t\tif err != nil {\n\t\t\treturn Events{}, nil, false, err\n\t\t}\n\t\towner := s.db.GetOwner(srcAddr)\n\t\tif owner == nil {\n\t\t\treturn Events{}, nil, false, fmt.Errorf(\"no owner for Jetton deposit in addressbook\")\n\t\t}\n\t\tif dstAddr != *owner {\n\t\t\taudit.LogTX(audit.Info, string(JettonDepositWallet), tx.Hash,\n\t\t\t\t\"sends transfer notification message not to owner\")\n\t\t\t// interpret it as an unknown message\n\t\t\tunknownMsgFound = true\n\t\t\tcontinue\n\t\t}\n\n\t\tvar (\n\t\t\tfrom          []byte\n\t\t\tfromWorkchain *int32\n\t\t)\n\t\tif notify.Sender != nil &&\n\t\t\t(notify.Sender.Type() == address.StdAddress || notify.Sender.Type() == address.VarAddress) {\n\t\t\tfrom = notify.Sender.Data()\n\t\t\twc := notify.Sender.Workchain()\n\t\t\tfromWorkchain = &wc\n\t\t}\n\t\tevents.ExternalIncomes = append(events.ExternalIncomes, ExternalIncome{\n\t\t\tUtime:         outMsg.CreatedAt,\n\t\t\tLt:            outMsg.CreatedLT,\n\t\t\tFrom:          from,\n\t\t\tFromWorkchain: fromWorkchain,\n\t\t\tTo:            srcAddr,\n\t\t\tAmount:        notify.Amount,\n\t\t\tComment:       notify.Comment,\n\t\t\tTxHash:        tx.Hash,\n\t\t})\n\t\tknownIncomeAmount.Add(knownIncomeAmount, notify.Amount.BigInt())\n\t}\n\treturn events, knownIncomeAmount, unknownMsgFound, nil\n}\n\nfunc (s *BlockScanner) processJettonDepositInMsg(tx *tlb.Transaction) (Events, *big.Int, bool, error) {\n\tvar events Events\n\tunknownMsgFound := false\n\ttotalWithdrawalsAmount := big.NewInt(0)\n\n\tif tx.IO.In == nil { // skip not decodable in_msg\n\t\taudit.LogTX(audit.Info, string(JettonDepositWallet), tx.Hash, \"transaction without in message\")\n\t\t// interpret it as an unknown message\n\t\treturn events, totalWithdrawalsAmount, true, nil\n\t}\n\n\tif tx.IO.In.MsgType != tlb.MsgTypeInternal { // skip not decodable in_msg\n\t\taudit.LogTX(audit.Info, string(JettonDepositWallet), tx.Hash, \"not internal in message\")\n\t\t// interpret it as an unknown message\n\t\treturn events, totalWithdrawalsAmount, true, nil\n\t}\n\n\tsuccess, err := checkTxForSuccess(tx)\n\tif err != nil {\n\t\treturn Events{}, nil, false, err\n\t}\n\n\tinMsg := tx.IO.In.AsInternal()\n\tdstAddr, err := AddressFromTonutilsAddress(inMsg.DstAddr)\n\tif err != nil {\n\t\treturn Events{}, nil, false, err\n\t}\n\n\ttransfer, err := DecodeJettonTransfer(inMsg)\n\tif err != nil {\n\t\tunknownMsgFound = true\n\t\treturn events, totalWithdrawalsAmount, unknownMsgFound, nil\n\t}\n\n\tif !success { // failed withdrawal from deposit jetton wallet\n\t\tevents.InternalWithdrawals = append(events.InternalWithdrawals, InternalWithdrawal{\n\t\t\tUtime:    inMsg.CreatedAt,\n\t\t\tLt:       inMsg.CreatedLT,\n\t\t\tFrom:     dstAddr,\n\t\t\tAmount:   transfer.Amount,\n\t\t\tMemo:     transfer.Comment,\n\t\t\tIsFailed: true,\n\t\t})\n\t\treturn events, totalWithdrawalsAmount, unknownMsgFound, nil\n\t}\n\n\t// success withdrawal from deposit jetton wallet\n\tif tx.OutMsgCount < 1 {\n\t\taudit.LogTX(audit.Error, string(JettonDepositWallet), tx.Hash, \"success Jettons transfer TX without out message\")\n\t\treturn Events{}, nil, true, fmt.Errorf(\"anomalous behavior of the deposit Jetton wallet\")\n\t}\n\ttotalWithdrawalsAmount.Add(totalWithdrawalsAmount, transfer.Amount.BigInt())\n\tdestType, destOk := s.db.GetWalletTypeByTonutilsAddress(transfer.Destination)\n\tif !destOk || destType != TonHotWallet {\n\t\taudit.LogTX(audit.Warning, string(JettonDepositWallet), tx.Hash,\n\t\t\tfmt.Sprintf(\"Jettons withdrawal from %v to %v (not to hot wallet)\",\n\t\t\t\tinMsg.DstAddr.String(), transfer.Destination.String()))\n\t\t// TODO: check balance calculation for unknown transactions for service transfers\n\t\t// not set unknownMsgFound = true to prevent service transfers interpretation as unknown\n\t\treturn Events{}, totalWithdrawalsAmount, false, nil\n\t}\n\tevents.InternalWithdrawals = append(events.InternalWithdrawals, InternalWithdrawal{\n\t\tUtime:    inMsg.CreatedAt,\n\t\tLt:       inMsg.CreatedLT,\n\t\tFrom:     dstAddr,\n\t\tAmount:   transfer.Amount,\n\t\tMemo:     transfer.Comment,\n\t\tIsFailed: false,\n\t})\n\n\treturn events, totalWithdrawalsAmount, unknownMsgFound, nil\n}\n"
  },
  {
    "path": "core/models.go",
    "content": "package core\n\nimport (\n\t\"context\"\n\t\"database/sql/driver\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/gobicycle/bicycle/config\"\n\t\"github.com/gofrs/uuid\"\n\t\"github.com/shopspring/decimal\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"github.com/xssnick/tonutils-go/tlb\"\n\t\"github.com/xssnick/tonutils-go/ton\"\n\t\"github.com/xssnick/tonutils-go/ton/wallet\"\n\t\"math/big\"\n\t\"time\"\n)\n\nconst (\n\tTonSymbol        = \"TON\"\n\tDefaultWorkchain = 0 // use only 0 workchain\n)\n\ntype IncomeSide = string\n\nconst (\n\tSideHotWallet IncomeSide = \"hot_wallet\"\n\tSideDeposit   IncomeSide = \"deposit\"\n)\n\ntype EventName = string\n\nconst (\n\tServiceWithdrawalEvent  EventName = \"service withdrawal\"\n\tInternalWithdrawalEvent EventName = \"internal withdrawal\"\n\tExternalWithdrawalEvent EventName = \"external withdrawal\"\n\tInitEvent               EventName = \"initialization\"\n)\n\ntype WalletType string\n\nconst (\n\tTonHotWallet        WalletType = \"ton_hot\"\n\tJettonHotWallet     WalletType = \"jetton_hot\"\n\tTonDepositWallet    WalletType = \"ton_deposit\"\n\tJettonDepositWallet WalletType = \"jetton_deposit\"\n\tJettonOwner         WalletType = \"owner\"\n)\n\ntype WithdrawalStatus string\n\nconst (\n\tPendingStatus    WithdrawalStatus = \"pending\"\n\tProcessingStatus WithdrawalStatus = \"processing\"\n\tProcessedStatus  WithdrawalStatus = \"processed\"\n\tFailedStatus     WithdrawalStatus = \"failed\"\n)\n\nvar (\n\tErrNotFound        = errors.New(\"not found\")\n\tErrTimeoutExceeded = errors.New(\"timeout exceeded\")\n)\n\ntype Address [32]byte // supports only MsgAddressInt addr_std$10 without anycast and 0 workchain\n\n// Scan implements Scanner for database/sql.\nfunc (a *Address) Scan(src interface{}) error {\n\tsrcB, ok := src.([]byte)\n\tif !ok {\n\t\treturn fmt.Errorf(\"can't scan %T into Address\", src)\n\t}\n\tif len(srcB) != 32 {\n\t\treturn fmt.Errorf(\"can't scan []byte of len %d into Address, want %d\", len(srcB), 32)\n\t}\n\tcopy(a[:], srcB)\n\treturn nil\n}\n\n// Value implements valuer for database/sql.\nfunc (a Address) Value() (driver.Value, error) {\n\treturn a[:], nil\n}\n\n// ToTonutilsAddressStd implements converter to ton-utils std Address type for default workchain !\nfunc (a Address) ToTonutilsAddressStd(flags byte) *address.Address {\n\treturn address.NewAddress(flags, DefaultWorkchain, a[:])\n}\n\n// ToUserFormat converts to user-friendly text format with testnet and bounce flags\nfunc (a Address) ToUserFormat() string {\n\taddr := a.ToTonutilsAddressStd(0)\n\taddr.SetTestnetOnly(config.Config.Testnet)\n\taddr.SetBounce(false)\n\treturn addr.String()\n}\n\nfunc (a Address) ToBytes() []byte {\n\treturn a[:]\n}\n\nfunc TonutilsAddressToUserFormat(addr *address.Address) string {\n\taddr.SetTestnetOnly(config.Config.Testnet)\n\taddr.SetBounce(false)\n\treturn addr.String()\n}\n\nfunc AddressFromBytes(data []byte) (Address, error) {\n\tif len(data) != 32 {\n\t\treturn Address{}, fmt.Errorf(\"invalid address len. Std addr len must be 32 bytes\")\n\t}\n\tvar res Address\n\tcopy(res[:], data)\n\treturn res, nil\n}\n\nfunc AddressFromTonutilsAddress(addr *address.Address) (Address, error) {\n\tif addr == nil {\n\t\treturn Address{}, fmt.Errorf(\"nil tonutils address\")\n\t}\n\tif addr.Type() != address.StdAddress {\n\t\treturn Address{}, fmt.Errorf(\"only std address supported\")\n\t}\n\treturn AddressFromBytes(addr.Data())\n}\n\nfunc AddressMustFromTonutilsAddress(addr *address.Address) Address {\n\tres, err := AddressFromTonutilsAddress(addr)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn res\n}\n\ntype AddressInfo struct {\n\tType   WalletType\n\tOwner  *Address\n\tUserID string\n}\n\ntype JettonWallet struct {\n\tAddress  *address.Address\n\tCurrency string\n}\n\ntype OwnerWallet struct {\n\tAddress  Address\n\tCurrency string\n}\n\ntype WalletData struct {\n\tSubwalletID uint32\n\tUserID      string\n\tCurrency    string\n\tType        WalletType\n\tAddress     Address\n}\n\ntype WithdrawalRequest struct {\n\tQueryID       string\n\tUserID        string\n\tCurrency      string\n\tAmount        Coins\n\tBounceable    bool\n\tIsInternal    bool\n\tDestination   Address\n\tComment       string\n\tBinaryComment string\n}\n\ntype WithdrawalData struct {\n\tQueryID string\n\tUserID  string\n\tStatus  WithdrawalStatus\n\tTxHash  []byte\n}\n\ntype ServiceWithdrawalRequest struct {\n\tFrom         Address\n\tJettonMaster *Address\n}\n\ntype ServiceWithdrawalTask struct {\n\tServiceWithdrawalRequest\n\tJettonAmount Coins\n\tMemo         uuid.UUID\n\tSubwalletID  uint32\n}\n\ntype ExternalWithdrawalTask struct {\n\tQueryID       int64\n\tCurrency      string\n\tAmount        Coins\n\tDestination   Address\n\tBounceable    bool\n\tComment       string\n\tBinaryComment string\n}\n\ntype InternalWithdrawal struct {\n\tUtime    uint32\n\tLt       uint64\n\tFrom     Address\n\tAmount   Coins\n\tMemo     string // uuid from comment\n\tIsFailed bool\n}\n\ntype SendingConfirmation struct {\n\tLt   uint64 // Lt of outgoing wallet message\n\tFrom Address\n\tMemo string // uuid from comment\n}\n\ntype ExternalWithdrawal struct {\n\tExtMsgUuid uuid.UUID\n\tUtime      uint32\n\tLt         uint64\n\tTo         Address\n\tAmount     Coins\n\tComment    string\n\tIsFailed   bool\n\tTxHash     []byte\n}\n\ntype JettonWithdrawalConfirmation struct {\n\tQueryId int64\n}\n\ntype InternalIncome struct {\n\tUtime    uint32\n\tLt       uint64 // will not fit in db bigint after 1.5 billion years\n\tFrom     Address\n\tTo       Address\n\tAmount   Coins\n\tMemo     string\n\tIsFailed bool\n\tTxHash   []byte\n}\n\ntype ExternalIncome struct {\n\tUtime         uint32\n\tLt            uint64\n\tFrom          []byte\n\tFromWorkchain *int32\n\tTo            Address\n\tAmount        Coins\n\tComment       string\n\tTxHash        []byte\n}\n\ntype Events struct {\n\tExternalIncomes         []ExternalIncome\n\tInternalIncomes         []InternalIncome\n\tSendingConfirmations    []SendingConfirmation\n\tInternalWithdrawals     []InternalWithdrawal\n\tExternalWithdrawals     []ExternalWithdrawal\n\tWithdrawalConfirmations []JettonWithdrawalConfirmation\n}\n\nfunc (e *Events) Append(ae Events) {\n\te.ExternalIncomes = append(e.ExternalIncomes, ae.ExternalIncomes...)\n\te.InternalIncomes = append(e.InternalIncomes, ae.InternalIncomes...)\n\te.SendingConfirmations = append(e.SendingConfirmations, ae.SendingConfirmations...)\n\te.InternalWithdrawals = append(e.InternalWithdrawals, ae.InternalWithdrawals...)\n\te.ExternalWithdrawals = append(e.ExternalWithdrawals, ae.ExternalWithdrawals...)\n\te.WithdrawalConfirmations = append(e.WithdrawalConfirmations, ae.WithdrawalConfirmations...)\n}\n\ntype BlockEvents struct {\n\tEvents\n\tBlock ShardBlockHeader\n}\n\ntype InternalWithdrawalTask struct {\n\tFrom        Address\n\tSubwalletID uint32\n\tLt          uint64\n\tCurrency    string\n}\n\ntype TotalIncome struct {\n\tDeposit  Address\n\tAmount   Coins\n\tCurrency string\n}\n\ntype TotalWithdrawalsAmount struct {\n\tPending    Coins\n\tProcessing Coins\n}\n\ntype Coins = decimal.Decimal\n\nfunc NewCoins(int *big.Int) Coins {\n\treturn decimal.NewFromBigInt(int, 0)\n}\n\nfunc ZeroCoins() Coins {\n\treturn decimal.New(0, 0)\n}\n\n// ShardBlockHeader\n// Block header for a specific shard mask attribute. Has only one parent.\ntype ShardBlockHeader struct {\n\t*ton.BlockIDExt\n\tNotMaster bool\n\tGenUtime  uint32\n\tStartLt   uint64\n\tEndLt     uint64\n\tParent    *ton.BlockIDExt\n}\n\ntype storage interface {\n\tGetExternalWithdrawalTasks(ctx context.Context, limit int) ([]ExternalWithdrawalTask, error)\n\tSaveTonWallet(ctx context.Context, walletData WalletData) error\n\tSaveJettonWallet(ctx context.Context, ownerAddress Address, walletData WalletData, notSaveOwner bool) error\n\tGetWalletType(address Address) (WalletType, bool)\n\tGetOwner(address Address) *Address\n\tGetUserID(address Address) (string, bool)\n\tGetWalletTypeByTonutilsAddress(address *address.Address) (WalletType, bool)\n\tSaveParsedBlockData(ctx context.Context, events BlockEvents) error\n\tGetTonInternalWithdrawalTasks(ctx context.Context, limit int) ([]InternalWithdrawalTask, error)\n\tGetJettonInternalWithdrawalTasks(ctx context.Context, forbiddenAddresses []Address, limit int) ([]InternalWithdrawalTask, error)\n\tCreateExternalWithdrawals(ctx context.Context, tasks []ExternalWithdrawalTask, extMsgUuid uuid.UUID, expiredAt time.Time) error\n\tGetTonHotWalletAddress(ctx context.Context) (Address, error)\n\tSetExpired(ctx context.Context) error\n\tSaveInternalWithdrawalTask(ctx context.Context, task InternalWithdrawalTask, expiredAt time.Time, memo uuid.UUID) error\n\tIsActualBlockData(ctx context.Context) (bool, int64, error)\n\tSaveWithdrawalRequest(ctx context.Context, w WithdrawalRequest) (int64, error)\n\tIsInProgressInternalWithdrawalRequest(ctx context.Context, dest Address, currency string) (bool, error)\n\tGetServiceHotWithdrawalTasks(ctx context.Context, limit int) ([]ServiceWithdrawalTask, error)\n\tUpdateServiceWithdrawalRequest(ctx context.Context, t ServiceWithdrawalTask, tonAmount Coins,\n\t\texpiredAt time.Time, filled bool) error\n\tGetServiceDepositWithdrawalTasks(ctx context.Context, limit int) ([]ServiceWithdrawalTask, error)\n\tGetJettonWallet(ctx context.Context, address Address) (*WalletData, bool, error)\n}\n\ntype blockchain interface {\n\tGetJettonWalletAddress(ctx context.Context, ownerWallet *address.Address, jettonMaster *address.Address) (*address.Address, error)\n\tGetTransactionIDsFromBlock(ctx context.Context, blockID *ton.BlockIDExt) ([]ton.TransactionShortInfo, error)\n\tGetTransactionFromBlock(ctx context.Context, blockID *ton.BlockIDExt, txID ton.TransactionShortInfo) (*tlb.Transaction, error)\n\tGenerateDefaultWallet(seed string, isHighload bool) (*wallet.Wallet, byte, uint32, error)\n\tGetJettonBalance(ctx context.Context, address Address, blockID *ton.BlockIDExt) (*big.Int, error)\n\tSendExternalMessage(ctx context.Context, msg *tlb.ExternalMessage) error\n\tGetAccountCurrentState(ctx context.Context, address *address.Address) (*big.Int, tlb.AccountStatus, error)\n\tGetLastJettonBalance(ctx context.Context, address *address.Address) (*big.Int, error)\n\tDeployTonWallet(ctx context.Context, wallet *wallet.Wallet) error\n}\n\ntype blocksTracker interface {\n\tNextBlock() (ShardBlockHeader, bool, error)\n\tStop()\n}\n\ntype Notificator interface {\n\tPublish(payload any) error\n}\n"
  },
  {
    "path": "core/proxy.go",
    "content": "package core\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"github.com/gobicycle/bicycle/config\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"github.com/xssnick/tonutils-go/tlb\"\n\t\"github.com/xssnick/tonutils-go/tvm/cell\"\n)\n\n// JettonProxy is a special contract wrapper that allow to control jetton wallet from TON wallet.\n// It is possible create few jetton proxies for single TON wallet (as owner) and control multiple jetton wallets.\n// Read about JettonProxy smart contract at README.md and https://github.com/gobicycle/ton-proxy-contract\ntype JettonProxy struct {\n\tOwner       *address.Address\n\tSubwalletID uint32\n\taddress     *address.Address\n\tstateInit   *tlb.StateInit\n}\n\nfunc NewJettonProxy(subwalletId uint32, owner *address.Address) (*JettonProxy, error) {\n\tif owner == nil {\n\t\treturn nil, fmt.Errorf(\"nil owner\")\n\t}\n\tstateInit := buildJettonProxyStateInit(subwalletId, owner)\n\tstateCell, err := tlb.ToCell(stateInit)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get state cell: %w\", err)\n\t}\n\taddr := address.NewAddress(0, DefaultWorkchain, stateCell.Hash())\n\n\treturn &JettonProxy{\n\t\tOwner:       owner,\n\t\tSubwalletID: subwalletId,\n\t\taddress:     addr,\n\t\tstateInit:   stateInit,\n\t}, nil\n}\n\nfunc buildJettonProxyStateInit(subwalletId uint32, owner *address.Address) *tlb.StateInit {\n\th, err := hex.DecodeString(config.JettonProxyContractCode)\n\tif err != nil {\n\t\tlog.Fatalf(\"decode JettonProxyContractCode hex error: %v\", err)\n\t}\n\tcode, err := cell.FromBOC(h)\n\tif err != nil {\n\t\tlog.Fatalf(\"parsing JettonProxyContractCode boc error: %v\", err)\n\t}\n\tdata := cell.BeginCell().\n\t\tMustStoreAddr(owner).\n\t\tMustStoreUInt(uint64(subwalletId), 32).\n\t\tEndCell()\n\tres := &tlb.StateInit{\n\t\tCode: code,\n\t\tData: data,\n\t}\n\treturn res\n}\n\n// Address returns address of jetton proxy contract\nfunc (p *JettonProxy) Address() *address.Address {\n\treturn p.address\n}\n\n// StateInit returns state init structure of jetton proxy contract\nfunc (p *JettonProxy) StateInit() *tlb.StateInit {\n\treturn p.stateInit\n}\n\n// BuildMessage wraps custom body payload to resend by proxy contract\nfunc (p *JettonProxy) BuildMessage(destination *address.Address, body *cell.Cell) *tlb.InternalMessage {\n\treturn &tlb.InternalMessage{\n\t\tIHRDisabled: true,\n\t\tBounce:      true,\n\t\tDstAddr:     destination,\n\t\tAmount:      tlb.FromNanoTONU(0), // proxy sends all TONs with mode == 128 + 32\n\t\tBody:        body,\n\t}\n}\n"
  },
  {
    "path": "core/wallets.go",
    "content": "package core\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/gobicycle/bicycle/audit\"\n\t\"github.com/gobicycle/bicycle/config\"\n\t\"github.com/gofrs/uuid\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/tonkeeper/tongo/boc\"\n\ttongoTlb \"github.com/tonkeeper/tongo/tlb\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"github.com/xssnick/tonutils-go/tlb\"\n\t\"github.com/xssnick/tonutils-go/ton/jetton\"\n\t\"github.com/xssnick/tonutils-go/ton/wallet\"\n\t\"github.com/xssnick/tonutils-go/tvm/cell\"\n\t\"math/big\"\n\t\"math/rand\"\n)\n\ntype Wallets struct {\n\tShard            byte\n\tTonHotWallet     *wallet.Wallet\n\tTonBasicWallet   *wallet.Wallet // basic V3 wallet to make other wallets with different subwallet_id\n\tJettonHotWallets map[string]JettonWallet\n}\n\n// InitWallets\n// Generates highload hot-wallet and map[currency]JettonWallet Jetton wallets, and saves to DB\n// TON highload hot-wallet (for seed and default subwallet_id) must be already active for success initialization.\nfunc InitWallets(\n\tctx context.Context,\n\tdb storage,\n\tbc blockchain,\n\tseed string,\n\tjettons map[string]config.Jetton,\n) (Wallets, error) {\n\n\tif config.Config.ColdWallet != nil && config.Config.ColdWallet.IsBounceable() {\n\t\t_, status, err := bc.GetAccountCurrentState(ctx, config.Config.ColdWallet)\n\t\tif err != nil {\n\t\t\treturn Wallets{}, err\n\t\t}\n\t\tlog.Infof(\"Cold wallet status: %s\", status)\n\t\tif status != tlb.AccountStatusActive {\n\t\t\treturn Wallets{}, fmt.Errorf(\"cold wallet address must be non-bounceable for not active wallet\")\n\t\t}\n\t}\n\n\ttonHotWallet, shard, subwalletId, err := initTonHotWallet(ctx, db, bc, seed)\n\tif err != nil {\n\t\treturn Wallets{}, err\n\t}\n\n\ttonBasicWallet, _, _, err := bc.GenerateDefaultWallet(seed, false)\n\tif err != nil {\n\t\treturn Wallets{}, err\n\t}\n\t// don't set TTL here because spec is not inherited by GetSubwallet method\n\n\tjettonHotWallets := make(map[string]JettonWallet)\n\tfor currency, j := range jettons {\n\t\tw, err := initJettonHotWallet(ctx, db, bc, tonHotWallet.Address(), j.Master, currency, subwalletId)\n\t\tif err != nil {\n\t\t\treturn Wallets{}, err\n\t\t}\n\t\tjettonHotWallets[currency] = w\n\t}\n\n\treturn Wallets{\n\t\tShard:            shard,\n\t\tTonHotWallet:     tonHotWallet,\n\t\tTonBasicWallet:   tonBasicWallet,\n\t\tJettonHotWallets: jettonHotWallets,\n\t}, nil\n}\n\nfunc initTonHotWallet(\n\tctx context.Context,\n\tdb storage,\n\tbc blockchain,\n\tseed string,\n) (\n\ttonHotWallet *wallet.Wallet,\n\tshard byte,\n\tsubwalletId uint32,\n\terr error,\n) {\n\ttonHotWallet, shard, subwalletId, err = bc.GenerateDefaultWallet(seed, true)\n\tif err != nil {\n\t\treturn nil, 0, 0, err\n\t}\n\thotSpec := tonHotWallet.GetSpec().(*wallet.SpecHighloadV2R2)\n\thotSpec.SetMessagesTTL(uint32(config.ExternalMessageLifetime.Seconds()))\n\n\taddr := AddressMustFromTonutilsAddress(tonHotWallet.Address())\n\talreadySaved := false\n\taddrFromDb, err := db.GetTonHotWalletAddress(ctx)\n\tif err == nil && addr != addrFromDb {\n\t\taudit.Log(audit.Error, string(TonHotWallet), InitEvent,\n\t\t\tfmt.Sprintf(\"Hot TON wallet address is not equal to the one stored in the database. Maybe seed was being changed. %s != %s\",\n\t\t\t\ttonHotWallet.Address().String(), addrFromDb.ToTonutilsAddressStd(0).String()))\n\t\treturn nil, 0, 0,\n\t\t\tfmt.Errorf(\"saved hot wallet not equal generated hot wallet. Maybe seed was being changed\")\n\t} else if !errors.Is(err, ErrNotFound) && err != nil {\n\t\treturn nil, 0, 0, err\n\t} else if err == nil {\n\t\talreadySaved = true\n\t}\n\n\tlog.Infof(\"Shard: %v\", shard)\n\tlog.Infof(\"TON hot wallet address: %v\", tonHotWallet.Address().String())\n\n\tbalance, status, err := bc.GetAccountCurrentState(ctx, tonHotWallet.Address())\n\tif err != nil {\n\t\treturn nil, 0, 0, err\n\t}\n\tif balance.Cmp(config.Config.Ton.HotWalletMin) == -1 { // hot wallet balance < TonHotWalletMinimumBalance\n\t\treturn nil, 0, 0,\n\t\t\tfmt.Errorf(\"hot wallet balance must be at least %v nanoTON\", config.Config.Ton.HotWalletMin)\n\t}\n\tif status != tlb.AccountStatusActive {\n\t\terr = bc.DeployTonWallet(ctx, tonHotWallet)\n\t\tif err != nil {\n\t\t\treturn nil, 0, 0, err\n\t\t}\n\t}\n\tif !alreadySaved {\n\t\terr = db.SaveTonWallet(ctx, WalletData{\n\t\t\tSubwalletID: uint32(wallet.DefaultSubwallet),\n\t\t\tCurrency:    TonSymbol,\n\t\t\tType:        TonHotWallet,\n\t\t\tAddress:     addr,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, 0, 0, err\n\t\t}\n\t}\n\treturn tonHotWallet, shard, subwalletId, nil\n}\n\nfunc initJettonHotWallet(\n\tctx context.Context,\n\tdb storage,\n\tbc blockchain,\n\ttonHotWallet, jettonMaster *address.Address,\n\tcurrency string,\n\tsubwalletId uint32,\n) (JettonWallet, error) {\n\t// not init or check balances of Jetton wallets, it is not required for the service to work\n\ta, err := bc.GetJettonWalletAddress(ctx, tonHotWallet, jettonMaster)\n\tif err != nil {\n\t\treturn JettonWallet{}, err\n\t}\n\tres := JettonWallet{Address: a, Currency: currency}\n\tlog.Infof(\"%v jetton hot wallet address: %v\", currency, a.String())\n\n\townerAddr, err := AddressFromTonutilsAddress(tonHotWallet)\n\tif err != nil {\n\t\treturn JettonWallet{}, err\n\t}\n\tjettonWalletAddr, err := AddressFromTonutilsAddress(a)\n\tif err != nil {\n\t\treturn JettonWallet{}, err\n\t}\n\n\twalletData, isPresented, err := db.GetJettonWallet(ctx, jettonWalletAddr)\n\tif err != nil {\n\t\treturn JettonWallet{}, err\n\t}\n\n\tif isPresented && walletData.Currency == currency {\n\t\treturn res, nil\n\t} else if isPresented && walletData.Currency != currency {\n\t\taudit.Log(audit.Error, string(JettonHotWallet), InitEvent,\n\t\t\tfmt.Sprintf(\"Hot Jetton wallets %s and %s have the same address %s\",\n\t\t\t\twalletData.Currency, currency, a.String()))\n\t\treturn JettonWallet{}, fmt.Errorf(\"jetton hot wallet address duplication\")\n\t}\n\n\terr = db.SaveJettonWallet(\n\t\tctx,\n\t\townerAddr,\n\t\tWalletData{\n\t\t\tSubwalletID: subwalletId,\n\t\t\tCurrency:    currency,\n\t\t\tType:        JettonHotWallet,\n\t\t\tAddress:     jettonWalletAddr,\n\t\t},\n\t\ttrue,\n\t)\n\tif err != nil {\n\t\treturn JettonWallet{}, err\n\t}\n\treturn res, nil\n}\n\nfunc buildComment(comment string) *cell.Cell {\n\troot := cell.BeginCell().MustStoreUInt(0, 32)\n\tif err := root.StoreStringSnake(comment); err != nil {\n\t\tlog.Fatalf(\"memo must fit into cell\")\n\t}\n\treturn root.EndCell()\n}\n\nfunc LoadComment(cell *cell.Cell) string {\n\tif cell == nil {\n\t\treturn \"\"\n\t}\n\tl := cell.BeginParse()\n\tif val, err := l.LoadUInt(32); err == nil && val == 0 {\n\t\tstr, err := l.LoadStringSnake()\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"load comment error: %v\", err)\n\t\t\treturn \"\"\n\t\t}\n\t\treturn str\n\t}\n\treturn \"\"\n}\n\n// WithdrawTONs\n// Send all TON from one wallet (and deploy it if needed) to another and destroy \"from\" wallet contract.\n// Wallet must be not empty.\nfunc WithdrawTONs(ctx context.Context, from, to *wallet.Wallet, comment string) error {\n\tif from == nil || to == nil || to.Address() == nil {\n\t\treturn fmt.Errorf(\"nil wallet\")\n\t}\n\tvar body *cell.Cell\n\tif comment != \"\" {\n\t\tbody = buildComment(comment)\n\t}\n\treturn from.Send(ctx, &wallet.Message{\n\t\tMode: 128 + 32, // 128 + 32 send all and destroy\n\t\tInternalMessage: &tlb.InternalMessage{\n\t\t\tIHRDisabled: true,\n\t\t\tBounce:      false,\n\t\t\tDstAddr:     to.Address(),\n\t\t\tAmount:      tlb.FromNanoTONU(0),\n\t\t\tBody:        body,\n\t\t},\n\t}, false)\n}\n\nfunc WithdrawJettons(\n\tctx context.Context,\n\tfrom, to *wallet.Wallet,\n\tjettonWallet *address.Address,\n\tforwardAmount tlb.Coins,\n\tamount Coins,\n\tcomment string,\n) error {\n\tif from == nil || to == nil || to.Address() == nil {\n\t\treturn fmt.Errorf(\"nil wallet\")\n\t}\n\tbody := MakeJettonTransferMessage(\n\t\tto.Address(),\n\t\tto.Address(),\n\t\tamount.BigInt(),\n\t\tforwardAmount,\n\t\trand.Int63(),\n\t\tcomment,\n\t\t\"\",\n\t)\n\treturn from.Send(ctx, &wallet.Message{\n\t\tMode: 128 + 32, // 128 + 32 send all and destroy\n\t\tInternalMessage: &tlb.InternalMessage{\n\t\t\tIHRDisabled: true,\n\t\t\tBounce:      true,\n\t\t\tDstAddr:     jettonWallet, // jetton wallet address\n\t\t\tAmount:      tlb.FromNanoTONU(0),\n\t\t\tBody:        body,\n\t\t},\n\t}, false)\n}\n\nfunc MakeJettonTransferMessage(\n\tdestination, responseDest *address.Address,\n\tamount *big.Int,\n\tforwardAmount tlb.Coins,\n\tqueryId int64,\n\tcomment string,\n\tbinaryComment string,\n) *cell.Cell {\n\n\tforwardPayload := cell.BeginCell().EndCell()\n\n\tif binaryComment != \"\" {\n\t\tc, err := decodeBinaryComment(binaryComment)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"decode binary comment error : %s\", err.Error())\n\t\t}\n\t\tforwardPayload = c\n\t} else if comment != \"\" {\n\t\tforwardPayload = buildComment(comment)\n\t}\n\n\tpayload, err := tlb.ToCell(jetton.TransferPayload{\n\t\tQueryID:             uint64(queryId),\n\t\tAmount:              tlb.FromNanoTON(amount),\n\t\tDestination:         destination,\n\t\tResponseDestination: responseDest,\n\t\tCustomPayload:       nil,\n\t\tForwardTONAmount:    forwardAmount,\n\t\tForwardPayload:      forwardPayload,\n\t})\n\n\tif err != nil {\n\t\tlog.Fatalf(\"jetton transfer message serialization error: %s\", err.Error())\n\t}\n\n\treturn payload\n}\n\n// decodeBinaryComment implements decoding of hex string and put it into cell with TLB scheme:\n// `binary_comment#b3ddcf7d {n:#} data:(SnakeData ~n) = InternalMsgBody;`\nfunc decodeBinaryComment(comment string) (*cell.Cell, error) {\n\n\tbitString, err := boc.BitStringFromFiftHex(comment)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc := boc.NewCell()\n\terr = c.WriteUint(0xb3ddcf7d, 32) // binary_comment#b3ddcf7d\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = tongoTlb.Marshal(c, tongoTlb.SnakeData(*bitString))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tb, err := c.ToBoc()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cell.FromBOC(b)\n}\n\nfunc BuildTonWithdrawalMessage(t ExternalWithdrawalTask) *wallet.Message {\n\n\tinternalMessage := tlb.InternalMessage{\n\t\tIHRDisabled: true,\n\t\tBounce:      t.Bounceable,\n\t\tDstAddr:     t.Destination.ToTonutilsAddressStd(0),\n\t\tAmount:      tlb.FromNanoTON(t.Amount.BigInt()),\n\t}\n\n\tif t.BinaryComment != \"\" {\n\t\tc, err := decodeBinaryComment(t.BinaryComment)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"decode binary comment error : %s\", err.Error())\n\t\t}\n\t\tinternalMessage.Body = c\n\t} else if t.Comment != \"\" {\n\t\tinternalMessage.Body = buildComment(t.Comment)\n\t} else {\n\t\tinternalMessage.Body = cell.BeginCell().EndCell()\n\t}\n\n\treturn &wallet.Message{\n\t\tMode:            3,\n\t\tInternalMessage: &internalMessage,\n\t}\n}\n\nfunc BuildJettonWithdrawalMessage(\n\tt ExternalWithdrawalTask,\n\thighloadWallet *wallet.Wallet,\n\tfromJettonWallet *address.Address,\n) *wallet.Message {\n\n\tbody := MakeJettonTransferMessage(\n\t\tt.Destination.ToTonutilsAddressStd(0),\n\t\thighloadWallet.Address(),\n\t\tt.Amount.BigInt(),\n\t\tconfig.JettonForwardAmount,\n\t\tt.QueryID,\n\t\tt.Comment,\n\t\tt.BinaryComment,\n\t)\n\n\treturn &wallet.Message{\n\t\tMode: 3,\n\t\tInternalMessage: &tlb.InternalMessage{\n\t\t\tIHRDisabled: true,\n\t\t\tBounce:      true,\n\t\t\tDstAddr:     fromJettonWallet,\n\t\t\tAmount:      config.JettonTransferTonAmount,\n\t\t\tBody:        body,\n\t\t},\n\t}\n}\n\nfunc BuildJettonProxyWithdrawalMessage(\n\tproxy JettonProxy,\n\tjettonWallet, tonWallet *address.Address,\n\tforwardAmount tlb.Coins,\n\tamount *big.Int,\n\tcomment string,\n) *wallet.Message {\n\tjettonTransferPayload := MakeJettonTransferMessage(\n\t\ttonWallet,\n\t\ttonWallet,\n\t\tamount,\n\t\tforwardAmount,\n\t\trand.Int63(),\n\t\tcomment,\n\t\t\"\",\n\t)\n\n\tmsg, err := tlb.ToCell(proxy.BuildMessage(jettonWallet, jettonTransferPayload))\n\tif err != nil {\n\t\tlog.Fatalf(\"build proxy message cell error: %v\", err)\n\t}\n\tbody := cell.BeginCell().MustStoreRef(msg).EndCell()\n\treturn &wallet.Message{\n\t\tMode: 3,\n\t\tInternalMessage: &tlb.InternalMessage{\n\t\t\tIHRDisabled: true,\n\t\t\tBounce:      true,\n\t\t\tDstAddr:     proxy.Address(),\n\t\t\tAmount:      config.JettonTransferTonAmount,\n\t\t\tBody:        body,\n\t\t\tStateInit:   proxy.StateInit(),\n\t\t},\n\t}\n}\n\nfunc buildJettonProxyServiceTonWithdrawalMessage(\n\tproxy JettonProxy,\n\ttonWallet *address.Address,\n\tmemo uuid.UUID,\n) *wallet.Message {\n\tmsg, err := tlb.ToCell(proxy.BuildMessage(tonWallet, buildComment(memo.String())))\n\tif err != nil {\n\t\tlog.Fatalf(\"build proxy message cell error: %v\", err)\n\t}\n\tbody := cell.BeginCell().MustStoreRef(msg).EndCell()\n\treturn &wallet.Message{\n\t\tMode: 3,\n\t\tInternalMessage: &tlb.InternalMessage{\n\t\t\tIHRDisabled: true,\n\t\t\tBounce:      true,\n\t\t\tDstAddr:     proxy.Address(),\n\t\t\tAmount:      config.JettonTransferTonAmount,\n\t\t\tBody:        body,\n\t\t\tStateInit:   proxy.StateInit(),\n\t\t},\n\t}\n}\n\nfunc buildTonFillMessage(\n\tto *address.Address,\n\tamount tlb.Coins,\n\tmemo uuid.UUID,\n) *wallet.Message {\n\treturn &wallet.Message{\n\t\tMode: 3,\n\t\tInternalMessage: &tlb.InternalMessage{\n\t\t\tIHRDisabled: true,\n\t\t\tBounce:      false,\n\t\t\tDstAddr:     to,\n\t\t\tAmount:      amount,\n\t\t\tBody:        buildComment(memo.String()),\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "core/withdrawal_processor.go",
    "content": "package core\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gobicycle/bicycle/audit\"\n\t\"github.com/gobicycle/bicycle/config\"\n\t\"github.com/gofrs/uuid\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"github.com/xssnick/tonutils-go/tlb\"\n\t\"github.com/xssnick/tonutils-go/ton/wallet\"\n)\n\ntype WithdrawalsProcessor struct {\n\tdb               storage\n\tbc               blockchain\n\twallets          Wallets\n\tcoldWallet       *address.Address\n\twg               *sync.WaitGroup\n\tgracefulShutdown atomic.Bool\n}\n\ntype internalWithdrawal struct {\n\tMemo uuid.UUID\n\tTask InternalWithdrawalTask\n}\n\ntype serviceWithdrawal struct {\n\tTonAmount Coins\n\tFilled    bool\n\tTask      ServiceWithdrawalTask\n}\n\ntype withdrawals struct {\n\tMessages []*wallet.Message\n\tExternal []ExternalWithdrawalTask\n\tInternal []internalWithdrawal\n\tService  []serviceWithdrawal\n}\n\nfunc NewWithdrawalsProcessor(\n\twg *sync.WaitGroup,\n\tdb storage,\n\tbc blockchain,\n\twallets Wallets,\n\tcoldWallet *address.Address,\n) *WithdrawalsProcessor {\n\tw := &WithdrawalsProcessor{\n\t\tdb:         db,\n\t\tbc:         bc,\n\t\twallets:    wallets,\n\t\tcoldWallet: coldWallet,\n\t\twg:         wg,\n\t}\n\treturn w\n}\n\nfunc (p *WithdrawalsProcessor) Start() {\n\tp.wg.Add(3)\n\tgo p.startWithdrawalsProcessor()\n\tgo p.startInternalTonWithdrawalsProcessor()\n\tgo p.startExpirationProcessor()\n}\n\nfunc (p *WithdrawalsProcessor) Stop() {\n\tp.gracefulShutdown.Store(true)\n}\n\nfunc (p *WithdrawalsProcessor) startWithdrawalsProcessor() {\n\tdefer p.wg.Done()\n\tlog.Infof(\"External withdrawal processor started\")\n\tfor {\n\t\tp.waitSync() // gracefulShutdown break must be after waitSync\n\t\tif p.gracefulShutdown.Load() {\n\t\t\tlog.Infof(\"External withdrawal processor stopped\")\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(config.ExternalWithdrawalPeriod)\n\t\tctx, cancel := context.WithTimeout(context.Background(), time.Second*50) // must be < ExternalWithdrawalPeriod\n\t\terr := p.makeColdWalletWithdrawals(ctx)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"make withdrawals to cold wallet error: %v\\n\", err)\n\t\t}\n\t\tw, err := p.buildWithdrawalMessages(ctx)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"make withdrawal messages error: %v\\n\", err)\n\t\t}\n\t\tif len(w.Messages) == 0 {\n\t\t\tcancel()\n\t\t\tcontinue\n\t\t}\n\t\textMsg, err := p.wallets.TonHotWallet.BuildExternalMessageForMany(ctx, w.Messages)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"build hotwallet external msg error: %v\\n\", err)\n\t\t}\n\t\tinfo, err := getHighLoadWalletExtMsgInfo(extMsg)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"get external message uuid error: %v\\n\", err)\n\t\t}\n\t\terr = p.db.CreateExternalWithdrawals(ctx, w.External, info.UUID, info.TTL)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"save external withdrawals error: %v\\n\", err)\n\t\t}\n\t\tfor _, sw := range w.Service {\n\t\t\terr = p.db.UpdateServiceWithdrawalRequest(ctx, sw.Task, sw.TonAmount, info.TTL, sw.Filled)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"update service withdrawal error: %v\\n\", err)\n\t\t\t}\n\t\t}\n\t\tfor _, iw := range w.Internal {\n\t\t\terr = p.db.SaveInternalWithdrawalTask(ctx, iw.Task, info.TTL, iw.Memo)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"save internal withdrawal error: %v\\n\", err)\n\t\t\t}\n\t\t}\n\t\terr = p.bc.SendExternalMessage(ctx, extMsg)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"send external msg error: %v\\n\", err)\n\t\t}\n\t\tcancel()\n\t}\n}\n\nfunc (p *WithdrawalsProcessor) buildWithdrawalMessages(ctx context.Context) (withdrawals, error) {\n\tvar (\n\t\tusedAddresses []Address\n\t\tres           withdrawals\n\t)\n\n\tbalances, err := p.getHotWalletBalances(ctx)\n\tif err != nil {\n\t\treturn withdrawals{}, fmt.Errorf(\"get hot wallet balance error: %s\", err.Error())\n\t}\n\n\tserviceTasks, err := p.db.GetServiceHotWithdrawalTasks(ctx, 250)\n\tif err != nil {\n\t\treturn withdrawals{}, err\n\t}\n\tfor _, t := range serviceTasks {\n\t\tif decreaseBalances(balances, TonSymbol, config.JettonTransferTonAmount.Nano()) {\n\t\t\tcontinue\n\t\t}\n\t\tmsg, w, err := p.buildServiceWithdrawalMessage(ctx, t)\n\t\tif err != nil {\n\t\t\treturn withdrawals{}, err\n\t\t}\n\t\tif len(msg) != 0 {\n\t\t\t// block scanner determines the uniqueness of the message in the batch by the dest address\n\t\t\t// the dest address will be the address of the proxy contract\n\t\t\t// TON deposit address is the dest addr for TON deposit filling message\n\t\t\t// so the address `t.From` is the dest address when checking the uniqueness\n\t\t\tusedAddresses = append(usedAddresses, t.From)\n\t\t\tres.Messages = append(res.Messages, msg...)\n\t\t\tres.Service = append(res.Service, w)\n\t\t} else {\n\t\t\t// save rejected service withdrawals\n\t\t\terr = p.db.UpdateServiceWithdrawalRequest(ctx, w.Task, w.TonAmount, time.Now(), w.Filled)\n\t\t\tif err != nil {\n\t\t\t\treturn withdrawals{}, err\n\t\t\t}\n\t\t}\n\t}\n\n\t// `internalTask.From` address is the address of deposit Jetton wallet\n\t// the dest address for uniqueness check is proxy contract address\n\t// so the proxy contract address must be deduplicated with usedAddresses in db query\n\tinternalTasks, err := p.db.GetJettonInternalWithdrawalTasks(ctx, usedAddresses, 250)\n\tif err != nil {\n\t\treturn withdrawals{}, err\n\t}\n\tfor _, t := range internalTasks {\n\t\tif len(res.Messages) > 250 {\n\t\t\tbreak\n\t\t}\n\t\tif decreaseBalances(balances, TonSymbol, config.JettonTransferTonAmount.Nano()) {\n\t\t\tcontinue\n\t\t}\n\t\tmsg, memo, err := p.buildJettonInternalWithdrawalMessage(ctx, t)\n\t\tif err != nil {\n\t\t\treturn withdrawals{}, err\n\t\t}\n\t\tif len(msg) != 0 {\n\t\t\tres.Messages = append(res.Messages, msg...)\n\t\t\tres.Internal = append(res.Internal, internalWithdrawal{\n\t\t\t\tTask: t,\n\t\t\t\tMemo: memo,\n\t\t\t})\n\t\t}\n\t}\n\n\t// not filter usedAddresses by DB and perform internal addresses checking and logging\n\texternalTasks, err := p.db.GetExternalWithdrawalTasks(ctx, 250)\n\tif err != nil {\n\t\treturn withdrawals{}, err\n\t}\n\tfor _, w := range externalTasks {\n\t\tif len(res.Messages) > 250 {\n\t\t\tbreak\n\t\t}\n\t\tt, ok := p.db.GetWalletType(w.Destination)\n\t\tif ok {\n\t\t\taudit.Log(audit.Warning, string(TonHotWallet), ExternalWithdrawalEvent,\n\t\t\t\tfmt.Sprintf(\"withdrawal task to internal %s address %s\", t, w.Destination.ToUserFormat()))\n\t\t\tcontinue\n\t\t}\n\t\tif decreaseBalances(balances, w.Currency, w.Amount.BigInt()) {\n\t\t\tcontinue\n\t\t}\n\t\tmsg := p.buildExternalWithdrawalMessage(w)\n\t\tres.Messages = append(res.Messages, msg)\n\t\tres.External = append(res.External, w)\n\t}\n\treturn res, nil\n}\n\nfunc (p *WithdrawalsProcessor) getHotWalletBalances(ctx context.Context) (map[string]*big.Int, error) {\n\tres := make(map[string]*big.Int)\n\tbalance, _, err := p.bc.GetAccountCurrentState(ctx, p.wallets.TonHotWallet.Address())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres[TonSymbol] = balance\n\tfor cur, w := range p.wallets.JettonHotWallets {\n\t\tbalance, err := p.bc.GetLastJettonBalance(ctx, w.Address)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres[cur] = balance\n\t}\n\treturn res, nil\n}\n\n// decreaseBalances returns true if balance < amount\nfunc decreaseBalances(balances map[string]*big.Int, currency string, amount *big.Int) bool {\n\tif currency == TonSymbol {\n\t\tif balances[TonSymbol].Cmp(amount) == -1 { // balance < amount\n\t\t\treturn true\n\t\t}\n\t\tbalances[TonSymbol].Sub(balances[TonSymbol], amount)\n\t\treturn false\n\t}\n\tif balances[currency].Cmp(amount) == -1 || // balance < amount\n\t\tbalances[TonSymbol].Cmp(config.JettonTransferTonAmount.Nano()) == -1 { // balance < JettonTransferTonAmount\n\t\treturn true\n\t}\n\tbalances[currency].Sub(balances[currency], amount)\n\tbalances[TonSymbol].Sub(balances[TonSymbol], config.JettonTransferTonAmount.Nano())\n\treturn false\n}\n\nfunc (p *WithdrawalsProcessor) buildJettonInternalWithdrawalMessage(\n\tctx context.Context,\n\ttask InternalWithdrawalTask,\n) (\n\t[]*wallet.Message,\n\tuuid.UUID,\n\terror,\n) {\n\tproxy, err := NewJettonProxy(task.SubwalletID, p.wallets.TonHotWallet.Address())\n\tif err != nil {\n\t\treturn nil, uuid.UUID{}, err\n\t}\n\tjettonWalletAddress := task.From.ToTonutilsAddressStd(0)\n\tbalance, err := p.bc.GetLastJettonBalance(ctx, jettonWalletAddress)\n\tif err != nil {\n\t\treturn nil, uuid.UUID{}, err\n\t}\n\tif balance.Cmp(config.Config.Jettons[task.Currency].WithdrawalCutoff) == 1 { // balance > MinimalJettonWithdrawalAmount\n\t\tmemo, err := uuid.NewV4()\n\t\tif err != nil {\n\t\t\treturn nil, uuid.UUID{}, err\n\t\t}\n\t\tmsg := BuildJettonProxyWithdrawalMessage(\n\t\t\t*proxy,\n\t\t\tjettonWalletAddress,\n\t\t\tp.wallets.TonHotWallet.Address(),\n\t\t\tconfig.JettonInternalForwardAmount,\n\t\t\tbalance,\n\t\t\tmemo.String(),\n\t\t)\n\t\treturn []*wallet.Message{msg}, memo, nil\n\t}\n\treturn []*wallet.Message{}, uuid.UUID{}, nil\n}\n\nfunc (p *WithdrawalsProcessor) buildServiceWithdrawalMessage(\n\tctx context.Context,\n\ttask ServiceWithdrawalTask,\n) (\n\t[]*wallet.Message,\n\tserviceWithdrawal,\n\terror,\n) {\n\tt, ok := p.db.GetWalletType(task.From)\n\tif !ok || !(t == JettonOwner || t == TonDepositWallet) {\n\t\treturn nil, serviceWithdrawal{}, fmt.Errorf(\"invalid service withdrawal address\")\n\t}\n\tif t == TonDepositWallet { // only fill TON deposit to send Jetton transfer message later\n\t\treturn p.buildServiceFilling(ctx, task)\n\t}\n\n\tif task.JettonMaster == nil { // full TON withdrawal from Jetton proxy\n\t\treturn p.buildServiceTonWithdrawal(ctx, task)\n\t}\n\t// Jetton withdrawal from Jetton wallet\n\treturn p.buildServiceJettonWithdrawal(ctx, task)\n}\n\nfunc (p *WithdrawalsProcessor) buildServiceFilling(\n\tctx context.Context,\n\ttask ServiceWithdrawalTask,\n) (\n\t[]*wallet.Message,\n\tserviceWithdrawal,\n\terror,\n) {\n\tdeposit := task.From.ToTonutilsAddressStd(0)\n\n\tjettonWallet, err := p.bc.GetJettonWalletAddress(\n\t\tctx,\n\t\tdeposit,\n\t\ttask.JettonMaster.ToTonutilsAddressStd(0))\n\tif err != nil {\n\t\treturn nil, serviceWithdrawal{}, err\n\t}\n\tjettonBalance, err := p.bc.GetLastJettonBalance(ctx, jettonWallet)\n\tif err != nil {\n\t\treturn nil, serviceWithdrawal{}, err\n\t}\n\n\tif jettonBalance.Cmp(big.NewInt(0)) == 0 {\n\t\taudit.Log(audit.Warning, string(TonDepositWallet), ServiceWithdrawalEvent,\n\t\t\tfmt.Sprintf(\"zero balance of Jettons %s on TON deposit address %s\",\n\t\t\t\ttask.JettonMaster.ToTonutilsAddressStd(0).String(),\n\t\t\t\tTonutilsAddressToUserFormat(deposit)))\n\t\treturn nil, serviceWithdrawal{\n\t\t\tTonAmount: ZeroCoins(),\n\t\t\tTask:      task,\n\t\t}, nil\n\t}\n\tmsg := buildTonFillMessage(deposit, config.JettonTransferTonAmount, task.Memo)\n\ttask.JettonAmount = NewCoins(jettonBalance)\n\treturn []*wallet.Message{msg}, serviceWithdrawal{\n\t\tTonAmount: ZeroCoins(),\n\t\tTask:      task,\n\t\tFilled:    true,\n\t}, nil\n}\n\nfunc (p *WithdrawalsProcessor) buildServiceTonWithdrawal(\n\tctx context.Context,\n\ttask ServiceWithdrawalTask,\n) (\n\t[]*wallet.Message,\n\tserviceWithdrawal,\n\terror,\n) {\n\tproxy, err := NewJettonProxy(task.SubwalletID, p.wallets.TonHotWallet.Address())\n\tif err != nil {\n\t\treturn nil, serviceWithdrawal{}, err\n\t}\n\ttonBalance, _, err := p.bc.GetAccountCurrentState(ctx, proxy.address)\n\tif err != nil {\n\t\treturn nil, serviceWithdrawal{}, err\n\t}\n\tres := serviceWithdrawal{\n\t\tTonAmount: NewCoins(tonBalance),\n\t\tTask:      task,\n\t}\n\tif tonBalance.Cmp(big.NewInt(0)) == 0 {\n\t\taudit.Log(audit.Warning, string(JettonOwner), ServiceWithdrawalEvent,\n\t\t\tfmt.Sprintf(\"zero balance of TONs on proxy address %s\", TonutilsAddressToUserFormat(proxy.address)))\n\t\treturn nil, res, nil\n\t}\n\tmsg := buildJettonProxyServiceTonWithdrawalMessage(*proxy, p.wallets.TonHotWallet.Address(), task.Memo)\n\treturn []*wallet.Message{msg}, res, nil\n}\n\nfunc (p *WithdrawalsProcessor) buildServiceJettonWithdrawal(\n\tctx context.Context,\n\ttask ServiceWithdrawalTask,\n) (\n\t[]*wallet.Message,\n\tserviceWithdrawal,\n\terror,\n) {\n\tproxy, err := NewJettonProxy(task.SubwalletID, p.wallets.TonHotWallet.Address())\n\tif err != nil {\n\t\treturn nil, serviceWithdrawal{}, err\n\t}\n\tjettonWallet, err := p.bc.GetJettonWalletAddress(ctx, proxy.address, task.JettonMaster.ToTonutilsAddressStd(0))\n\tif err != nil {\n\t\treturn nil, serviceWithdrawal{}, err\n\t}\n\tt, ok := p.db.GetWalletTypeByTonutilsAddress(jettonWallet)\n\tif ok {\n\t\taudit.Log(audit.Warning, string(JettonOwner), ServiceWithdrawalEvent,\n\t\t\tfmt.Sprintf(\"service withdrawal from known internal %s address %s rejected\",\n\t\t\t\tt, TonutilsAddressToUserFormat(jettonWallet)))\n\t\treturn nil, serviceWithdrawal{\n\t\t\tTonAmount: ZeroCoins(),\n\t\t\tTask:      task,\n\t\t}, nil\n\t}\n\n\tjettonBalance, err := p.bc.GetLastJettonBalance(ctx, jettonWallet)\n\tif err != nil {\n\t\treturn nil, serviceWithdrawal{}, err\n\t}\n\n\tif jettonBalance.Cmp(big.NewInt(0)) == 0 {\n\t\taudit.Log(audit.Warning, string(JettonOwner), ServiceWithdrawalEvent,\n\t\t\tfmt.Sprintf(\"zero %s Jetton balance on proxy address %s\",\n\t\t\t\ttask.JettonMaster.ToTonutilsAddressStd(0).String(),\n\t\t\t\tTonutilsAddressToUserFormat(proxy.address)))\n\t\treturn nil, serviceWithdrawal{\n\t\t\tTonAmount: ZeroCoins(),\n\t\t\tTask:      task,\n\t\t}, nil\n\t}\n\ttask.JettonAmount = NewCoins(jettonBalance)\n\tres := serviceWithdrawal{\n\t\tTonAmount: ZeroCoins(),\n\t\tTask:      task,\n\t}\n\n\tmsg := BuildJettonProxyWithdrawalMessage(\n\t\t*proxy,\n\t\tjettonWallet,\n\t\tp.wallets.TonHotWallet.Address(),\n\t\ttlb.FromNanoTONU(0), // zero forward amount to prevent notification sending and incorrect internal income invoking\n\t\tjettonBalance,\n\t\ttask.Memo.String(),\n\t)\n\treturn []*wallet.Message{msg}, res, nil\n}\n\nfunc (p *WithdrawalsProcessor) buildExternalWithdrawalMessage(wt ExternalWithdrawalTask) *wallet.Message {\n\tif wt.Currency == TonSymbol {\n\t\treturn BuildTonWithdrawalMessage(wt)\n\t}\n\tjw := p.wallets.JettonHotWallets[wt.Currency]\n\treturn BuildJettonWithdrawalMessage(wt, p.wallets.TonHotWallet, jw.Address)\n}\n\nfunc (p *WithdrawalsProcessor) startExpirationProcessor() {\n\tlog.Infof(\"Expiration processor started\")\n\tdefer p.wg.Done()\n\tfor {\n\t\tp.waitSync() // gracefulShutdown break must be after waitSync\n\t\tif p.gracefulShutdown.Load() {\n\t\t\tlog.Infof(\"Expiration processor stopped\")\n\t\t\tbreak\n\t\t}\n\t\tctx, cancel := context.WithTimeout(context.Background(), time.Second*3) // must be < ExpirationProcessorPeriod\n\t\terr := p.db.SetExpired(ctx)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"set expired withdrawals error: %v\", err)\n\t\t}\n\t\tcancel()\n\t\ttime.Sleep(config.ExpirationProcessorPeriod)\n\t}\n}\n\nfunc (p *WithdrawalsProcessor) startInternalTonWithdrawalsProcessor() {\n\tdefer p.wg.Done()\n\tlog.Infof(\"Internal TON withdrawal processor started\")\n\tfor {\n\t\tp.waitSync() // gracefulShutdown break must be after waitSync\n\t\tif p.gracefulShutdown.Load() {\n\t\t\tlog.Infof(\"Internal TON withdrawal processor stopped\")\n\t\t\tbreak\n\t\t}\n\t\tctx, cancel := context.WithTimeout(context.Background(), time.Second*120) // TODO: split context\n\t\tserviceTasks, err := p.db.GetServiceDepositWithdrawalTasks(ctx, 5)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"get service withdrawal tasks error: %v\", err)\n\t\t}\n\t\tfor _, task := range serviceTasks {\n\t\t\terr = p.serviceWithdrawJettons(ctx, task)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Jettons service internal withdrawal error: %v\", err)\n\t\t\t}\n\t\t\ttime.Sleep(time.Millisecond * 50)\n\t\t}\n\n\t\tinternalTasks, err := p.db.GetTonInternalWithdrawalTasks(ctx, 40) // context limitation\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"get internal withdrawal tasks error: %v\", err)\n\t\t}\n\t\tfor _, task := range internalTasks {\n\t\t\terr = p.withdrawTONsFromDeposit(ctx, task)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"TONs internal withdrawal error: %v\", err)\n\t\t\t}\n\t\t\ttime.Sleep(time.Millisecond * 50)\n\t\t}\n\t\tcancel()\n\t\ttime.Sleep(config.InternalWithdrawalPeriod)\n\t}\n}\n\nfunc (p *WithdrawalsProcessor) withdrawTONsFromDeposit(ctx context.Context, task InternalWithdrawalTask) error {\n\tsubwallet, err := p.wallets.TonBasicWallet.GetSubwallet(task.SubwalletID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tspec := subwallet.GetSpec().(*wallet.SpecV3)\n\tspec.SetMessagesTTL(uint32(config.ExternalMessageLifetime.Seconds()))\n\n\tbalance, state, err := p.bc.GetAccountCurrentState(ctx, subwallet.Address())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif state == tlb.AccountStatusNonExist {\n\t\treturn nil\n\t}\n\tif balance.Cmp(config.Config.Ton.Withdrawal) == 1 { // Balance > MinimalTonWithdrawalAmount\n\t\tmemo, err := uuid.NewV4()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = p.db.SaveInternalWithdrawalTask(ctx, task, time.Now().Add(config.ExternalMessageLifetime), memo)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// time.Now().Add(config.ExternalMessageLifetime) and real TTL\n\t\t// should be very close since the withdrawal occurs immediately\n\t\terr = WithdrawTONs(ctx, subwallet, p.wallets.TonHotWallet, memo.String())\n\t\tif err != nil {\n\t\t\taudit.Log(audit.Info, string(TonDepositWallet), InternalWithdrawalEvent,\n\t\t\t\tfmt.Sprintf(\"TONs internal withdrawal from deposit %s error: %s\",\n\t\t\t\t\ttask.From.ToUserFormat(), err.Error()))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *WithdrawalsProcessor) serviceWithdrawJettons(ctx context.Context, task ServiceWithdrawalTask) error {\n\tsubwallet, err := p.wallets.TonBasicWallet.GetSubwallet(task.SubwalletID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tspec := subwallet.GetSpec().(*wallet.SpecV3)\n\tspec.SetMessagesTTL(uint32(config.ExternalMessageLifetime.Seconds()))\n\n\t_, state, err := p.bc.GetAccountCurrentState(ctx, subwallet.Address())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif state == tlb.AccountStatusNonExist {\n\t\treturn nil\n\t}\n\n\tjettonWallet, err := p.bc.GetJettonWalletAddress(ctx, subwallet.Address(), task.JettonMaster.ToTonutilsAddressStd(0))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = p.db.UpdateServiceWithdrawalRequest(ctx, task, ZeroCoins(),\n\t\ttime.Now().Add(config.ExternalMessageLifetime), false)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// time.Now().Add(config.ExternalMessageLifetime) and real TTL\n\t// should be very close since the withdrawal occurs immediately\n\terr = WithdrawJettons(ctx, subwallet, p.wallets.TonHotWallet, jettonWallet, tlb.FromNanoTONU(0),\n\t\ttask.JettonAmount, task.Memo.String()) // zero forward TON amount to prevent notify message invoking\n\tif err != nil {\n\t\tlog.Errorf(\"Jettons service withdrawal error: %v\", err)\n\t\taudit.Log(audit.Info, string(TonDepositWallet), ServiceWithdrawalEvent,\n\t\t\tfmt.Sprintf(\"Jettons service withdrawal from deposit %s error: %s\",\n\t\t\t\ttask.From.ToUserFormat(), err.Error()))\n\t}\n\treturn nil\n}\n\nfunc (p *WithdrawalsProcessor) waitSync() {\n\tfor {\n\t\tif p.gracefulShutdown.Load() {\n\t\t\tlog.Infof(\"WaitSync interrupted\")\n\t\t\tbreak\n\t\t}\n\t\tctx, cancel := context.WithTimeout(context.Background(), time.Second*2)\n\t\tisSynced, _, err := p.db.IsActualBlockData(ctx)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"check sync error: %v\", err)\n\t\t}\n\t\tif isSynced {\n\t\t\tcancel()\n\t\t\tbreak\n\t\t}\n\t\tcancel()\n\t\ttime.Sleep(time.Second * 3)\n\t}\n}\n\nfunc (p *WithdrawalsProcessor) makeColdWalletWithdrawals(ctx context.Context) error {\n\tif p.coldWallet == nil {\n\t\treturn nil\n\t}\n\n\ttonBalance, _, err := p.bc.GetAccountCurrentState(ctx, p.wallets.TonHotWallet.Address())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdest := AddressMustFromTonutilsAddress(p.coldWallet)\n\n\tfor cur, jw := range p.wallets.JettonHotWallets {\n\t\tinProgress, err := p.db.IsInProgressInternalWithdrawalRequest(ctx, dest, cur)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif inProgress {\n\t\t\tcontinue\n\t\t}\n\t\tjettonBalance, err := p.bc.GetLastJettonBalance(ctx, jw.Address)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif jettonBalance.Cmp(config.Config.Jettons[cur].HotWalletMaxCutoff) != 1 { // jettonBalance <= HotWalletMaxCutoff\n\t\t\tcontinue\n\t\t}\n\t\tjettonAmount := big.NewInt(0)\n\t\tu, err := uuid.NewV4()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tjettonAmount.Sub(jettonBalance, config.Config.Jettons[cur].HotWalletResidual)\n\t\ttonBalance.Sub(tonBalance, config.JettonTransferTonAmount.Nano())\n\t\treq := WithdrawalRequest{\n\t\t\tCurrency:    jw.Currency,\n\t\t\tAmount:      NewCoins(jettonAmount),\n\t\t\tBounceable:  true,\n\t\t\tDestination: dest,\n\t\t\tIsInternal:  true,\n\t\t\tQueryID:     u.String(),\n\t\t}\n\t\t_, err = p.db.SaveWithdrawalRequest(ctx, req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlog.Infof(\"%v withdrawal to cold wallet saved\", cur)\n\t}\n\n\tinProgress, err := p.db.IsInProgressInternalWithdrawalRequest(ctx, dest, TonSymbol)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif inProgress {\n\t\treturn nil\n\t}\n\n\tif tonBalance.Cmp(config.Config.Ton.HotWalletMax) != 1 { // tonBalance <= HotWalletMax\n\t\treturn nil\n\t}\n\n\ttonAmount := big.NewInt(0)\n\tu, err := uuid.NewV4()\n\tif err != nil {\n\t\treturn err\n\t}\n\ttonAmount.Sub(tonBalance, config.Config.Ton.HotWalletResidual)\n\treq := WithdrawalRequest{\n\t\tCurrency:    TonSymbol,\n\t\tAmount:      NewCoins(tonAmount),\n\t\tBounceable:  p.coldWallet.IsBounceable(),\n\t\tDestination: dest,\n\t\tIsInternal:  true,\n\t\tQueryID:     u.String(),\n\t}\n\n\t_, err = p.db.SaveWithdrawalRequest(ctx, req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Infof(\"TON withdrawal to cold wallet saved\")\n\treturn nil\n}\n"
  },
  {
    "path": "db/db.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"unicode/utf8\"\n\n\t\"github.com/gobicycle/bicycle/audit\"\n\t\"github.com/gobicycle/bicycle/config\"\n\t\"github.com/gobicycle/bicycle/core\"\n\t\"github.com/gofrs/uuid\"\n\t\"github.com/jackc/pgx/v4\"\n\t\"github.com/jackc/pgx/v4/pgxpool\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/xssnick/tonutils-go/address\"\n\t\"github.com/xssnick/tonutils-go/ton\"\n\t\"github.com/xssnick/tonutils-go/ton/wallet\"\n)\n\ntype Connection struct {\n\tclient      *pgxpool.Pool\n\taddressBook addressBook\n}\n\ntype addressBook struct {\n\taddresses map[core.Address]core.AddressInfo\n\tmutex     sync.Mutex\n}\n\nfunc (ab *addressBook) get(address core.Address) (core.AddressInfo, bool) {\n\tab.mutex.Lock()\n\tt, ok := ab.addresses[address]\n\tab.mutex.Unlock()\n\treturn t, ok\n}\n\nfunc (ab *addressBook) put(address core.Address, t core.AddressInfo) {\n\tab.mutex.Lock()\n\tab.addresses[address] = t\n\tab.mutex.Unlock()\n}\n\nfunc NewConnection(URI string) (*Connection, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*20)\n\tdefer cancel()\n\tclient, err := pgxpool.Connect(ctx, URI)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"connection err: %v\", err)\n\t}\n\tconn := Connection{client, addressBook{}}\n\treturn &conn, nil\n}\n\nfunc (c *Connection) GetWalletType(address core.Address) (core.WalletType, bool) {\n\tinfo, ok := c.addressBook.get(address)\n\treturn info.Type, ok\n}\n\nfunc (c *Connection) GetUserID(address core.Address) (string, bool) {\n\tinfo, ok := c.addressBook.get(address)\n\treturn info.UserID, ok\n}\n\n// GetOwner returns owner for jetton deposit from address book and nil for other types\nfunc (c *Connection) GetOwner(address core.Address) *core.Address {\n\tinfo, ok := c.addressBook.get(address)\n\tif ok && info.Type == core.JettonDepositWallet && info.Owner == nil {\n\t\tlog.Fatalf(\"must be owner address in address book for jetton deposit\")\n\t}\n\treturn info.Owner\n}\n\nfunc (c *Connection) GetWalletTypeByTonutilsAddress(address *address.Address) (core.WalletType, bool) {\n\ta, err := core.AddressFromTonutilsAddress(address)\n\tif err != nil {\n\t\treturn \"\", false\n\t}\n\treturn c.GetWalletType(a)\n}\n\n// GetLastSubwalletID returns last (greatest) used subwallet_id from DB\n// numeration starts from wallet.DefaultSubwallet (this number reserved for main hot wallet)\n// returns wallet.DefaultSubwallet if table is empty\nfunc (c *Connection) GetLastSubwalletID(ctx context.Context) (uint32, error) {\n\tvar id uint32\n\terr := c.client.QueryRow(ctx, `\n\t\tSELECT COALESCE(MAX(subwallet_id), $1)\n\t\tFROM payments.ton_wallets\n\t`, wallet.DefaultSubwallet).Scan(&id)\n\treturn id, err\n}\n\nfunc (c *Connection) SaveTonWallet(ctx context.Context, walletData core.WalletData) error {\n\t_, err := c.client.Exec(ctx, `\n\t\tINSERT INTO payments.ton_wallets (\n\t\tuser_id,\n\t\tsubwallet_id,\n\t\ttype,\n\t\taddress)\n\t\tVALUES ($1, $2, $3,$4)         \n\t`,\n\t\twalletData.UserID,\n\t\twalletData.SubwalletID,\n\t\twalletData.Type,\n\t\twalletData.Address,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.addressBook.put(walletData.Address, core.AddressInfo{Type: walletData.Type, Owner: nil, UserID: walletData.UserID})\n\treturn nil\n}\n\nfunc (c *Connection) GetJettonWallet(ctx context.Context, address core.Address) (*core.WalletData, bool, error) {\n\td := core.WalletData{\n\t\tAddress: address,\n\t}\n\terr := c.client.QueryRow(ctx, `\n\t\tSELECT subwallet_id, user_id, currency, type\n\t\tFROM payments.jetton_wallets\n\t\tWHERE  address = $1\n\t`, address).Scan(&d.SubwalletID, &d.UserID, &d.Currency, &d.Type)\n\tif errors.Is(err, pgx.ErrNoRows) {\n\t\treturn nil, false, nil\n\t}\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\treturn &d, true, nil\n}\n\nfunc (c *Connection) SaveJettonWallet(\n\tctx context.Context,\n\townerAddress core.Address,\n\twalletData core.WalletData,\n\tnotSaveOwner bool,\n) error {\n\ttx, err := c.client.Begin(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback(ctx)\n\n\tif !notSaveOwner {\n\t\t_, err = tx.Exec(ctx, `\n\t\tINSERT INTO payments.ton_wallets (\n\t\tuser_id,\n\t\tsubwallet_id,\n\t\ttype,\n\t\taddress)\n\t\tVALUES ($1, $2, $3,$4)            \n\t`,\n\t\t\twalletData.UserID,\n\t\t\twalletData.SubwalletID,\n\t\t\tcore.JettonOwner,\n\t\t\townerAddress,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t_, err = tx.Exec(ctx, `\n\t\tINSERT INTO payments.jetton_wallets (\n\t\tuser_id,\n\t\tsubwallet_id,\n\t\tcurrency,\n\t\ttype,\n\t\taddress)\n\t\tVALUES ($1, $2, $3, $4, $5)\n\t`,\n\t\twalletData.UserID,\n\t\twalletData.SubwalletID,\n\t\twalletData.Currency,\n\t\twalletData.Type,\n\t\twalletData.Address,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = tx.Commit(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif walletData.Type == core.JettonDepositWallet {\n\t\t// only jetton deposit owners tracked by address book\n\t\t// hot TON wallet also owner of jetton hot wallets\n\t\t// cold wallets excluded from address book\n\t\tc.addressBook.put(ownerAddress, core.AddressInfo{Type: core.JettonOwner, Owner: nil, UserID: walletData.UserID})\n\t}\n\tc.addressBook.put(walletData.Address, core.AddressInfo{Type: walletData.Type, Owner: &ownerAddress, UserID: walletData.UserID})\n\treturn nil\n}\n\nfunc (c *Connection) GetTonWalletsAddresses(\n\tctx context.Context,\n\tuserID string,\n\ttypes []core.WalletType,\n) (\n\t[]core.Address,\n\terror,\n) {\n\tif types == nil {\n\t\ttypes = make([]core.WalletType, 0)\n\t}\n\trows, err := c.client.Query(ctx, `\n\t\tSELECT address\n\t\tFROM payments.ton_wallets\n\t\tWHERE user_id = $1 AND type=ANY($2)\n\t`, userID, types)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar res []core.Address\n\tfor rows.Next() {\n\t\tvar a core.Address\n\t\terr = rows.Scan(&a)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, a)\n\t}\n\tif rows.Err() != nil {\n\t\treturn nil, rows.Err()\n\t}\n\treturn res, nil\n}\n\nfunc (c *Connection) GetJettonOwnersAddresses(\n\tctx context.Context,\n\tuserID string,\n\ttypes []core.WalletType,\n) (\n\t[]core.OwnerWallet,\n\terror,\n) {\n\tif types == nil {\n\t\ttypes = make([]core.WalletType, 0)\n\t}\n\trows, err := c.client.Query(ctx, `\n\t\tSELECT tw.address, jw.currency\n\t\tFROM payments.jetton_wallets jw\n\t\tLEFT JOIN payments.ton_wallets tw ON jw.subwallet_id = tw.subwallet_id \n\t\tWHERE jw.user_id = $1 AND jw.type=ANY($2)\n\t`, userID, types)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar res []core.OwnerWallet\n\tfor rows.Next() {\n\t\tvar ow core.OwnerWallet\n\t\terr = rows.Scan(&ow.Address, &ow.Currency)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, ow)\n\t}\n\tif rows.Err() != nil {\n\t\treturn nil, rows.Err()\n\t}\n\treturn res, nil\n}\n\nfunc (c *Connection) LoadAddressBook(ctx context.Context) error {\n\tres := make(map[core.Address]core.AddressInfo)\n\tvar (\n\t\taddr   core.Address\n\t\tt      core.WalletType\n\t\tuserID string\n\t)\n\n\trows, err := c.client.Query(ctx, `\n\t\tSELECT address, type, user_id\n\t\tFROM payments.ton_wallets\n\t`)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\terr = rows.Scan(&addr, &t, &userID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tres[addr] = core.AddressInfo{Type: t, Owner: nil, UserID: userID}\n\t}\n\tif rows.Err() != nil {\n\t\treturn rows.Err()\n\t}\n\trows, err = c.client.Query(ctx, `\n\t\tSELECT jw.address, jw.type, tw.address, jw.user_id\n\t\tFROM payments.jetton_wallets jw\n\t\tLEFT JOIN payments.ton_wallets tw ON jw.subwallet_id = tw.subwallet_id\n\t`)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar owner core.Address\n\t\terr = rows.Scan(&addr, &t, &owner, &userID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tres[addr] = core.AddressInfo{Type: t, Owner: &owner, UserID: userID}\n\t}\n\tif rows.Err() != nil {\n\t\treturn rows.Err()\n\t}\n\n\tc.addressBook.addresses = res\n\tlog.Info(\"Address book loaded\")\n\treturn nil\n}\n\nfunc stripInvalidUTF8(s string) string {\n\tb := []byte(s)\n\tout := b[:0]\n\tfor len(b) > 0 {\n\t\tr, size := utf8.DecodeRune(b)\n\t\tif r != utf8.RuneError || size > 1 {\n\t\t\tout = append(out, b[:size]...)\n\t\t}\n\t\tb = b[size:]\n\t}\n\treturn strings.Replace(string(out), \"\\x00\", \"\", -1) // PostgreSQL doesn't support storing NULL (\\0x00) characters in text fields\n}\n\nfunc saveExternalIncome(ctx context.Context, tx pgx.Tx, inc core.ExternalIncome) error {\n\tinc.Comment = stripInvalidUTF8(inc.Comment) // PostgreSQL doesn't support storing invalid utf-8 characters\n\t_, err := tx.Exec(ctx, `\n\t\tINSERT INTO payments.external_incomes (\n\t\tlt,\n\t\tutime,\n\t\tdeposit_address,\n\t\tpayer_address,\n\t\tamount,\n\t\tcomment,\n\t\tpayer_workchain,\n\t\ttx_hash)\n\t\tVALUES ($1, $2, $3, $4, $5, $6, $7, $8)                                               \n\t`,\n\t\tinc.Lt,\n\t\ttime.Unix(int64(inc.Utime), 0),\n\t\tinc.To,\n\t\tinc.From,\n\t\tinc.Amount,\n\t\tinc.Comment,\n\t\tinc.FromWorkchain,\n\t\tinc.TxHash,\n\t)\n\treturn err\n}\n\nfunc (c *Connection) saveInternalIncome(ctx context.Context, tx pgx.Tx, inc core.InternalIncome) error {\n\tmemo, err := uuid.FromString(inc.Memo)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\twType, ok := c.GetWalletType(inc.From)\n\tvar from core.Address\n\tif ok && wType == core.JettonOwner { // convert jetton owner address to jetton wallet address\n\t\terr = tx.QueryRow(ctx, `\n\t\t\tSELECT jw.address\n\t\t\tFROM payments.ton_wallets tw\n\t\t\tLEFT JOIN payments.jetton_wallets jw ON tw.subwallet_id = jw.subwallet_id\n\t\t\tWHERE tw.address = $1\n\t\t`, inc.From).Scan(&from)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tfrom = inc.From\n\t}\n\n\t_, err = tx.Exec(ctx, `\n\t\tINSERT INTO payments.internal_incomes (\n\t\tlt,\n\t\tutime,\n\t\tdeposit_address,\n\t\tamount,\n\t\tmemo)\n\t\tVALUES ($1, $2, $3, $4, $5)                                               \n\t`,\n\t\tinc.Lt,\n\t\ttime.Unix(int64(inc.Utime), 0),\n\t\tfrom,\n\t\tinc.Amount,\n\t\tmemo,\n\t)\n\treturn err\n}\n\nfunc (c *Connection) SaveWithdrawalRequest(ctx context.Context, w core.WithdrawalRequest) (int64, error) {\n\n\tvar queryID int64\n\n\terr := c.client.QueryRow(ctx, `\n\t\tINSERT INTO payments.withdrawal_requests (\n\t\tuser_id,\n\t\tuser_query_id,\n\t\tamount,\n\t\tcurrency,\n\t\tbounceable,\n\t\tdest_address,\n\t\tcomment,\n\t\tis_internal,\n\t\tbinary_comment\n\t)\n\t\tVALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n\t\tRETURNING query_id\n\t`,\n\t\tw.UserID,\n\t\tw.QueryID,\n\t\tw.Amount,\n\t\tw.Currency,\n\t\tw.Bounceable,\n\t\tw.Destination,\n\t\tw.Comment,\n\t\tw.IsInternal,\n\t\tw.BinaryComment,\n\t).Scan(&queryID)\n\treturn queryID, err\n}\n\nfunc (c *Connection) SaveServiceWithdrawalRequest(ctx context.Context, w core.ServiceWithdrawalRequest) (\n\tuuid.UUID,\n\terror,\n) {\n\tvar memo uuid.UUID\n\terr := c.client.QueryRow(ctx, `\n\t\tINSERT INTO payments.service_withdrawal_requests (\n\t\tfrom_address,\n\t\tjetton_master\t\t\n\t)\n\t\tVALUES ($1, $2)\n\t\tRETURNING memo\n\t`,\n\t\tw.From,\n\t\tw.JettonMaster,\n\t).Scan(&memo)\n\treturn memo, err\n}\n\nfunc (c *Connection) UpdateServiceWithdrawalRequest(\n\tctx context.Context,\n\tt core.ServiceWithdrawalTask,\n\ttonAmount core.Coins,\n\texpiredAt time.Time,\n\tfilled bool,\n) error {\n\t_, err := c.client.Exec(ctx, `\n\t\t\tUPDATE payments.service_withdrawal_requests\n\t\t\tSET\n\t\t\t    ton_amount = $1,\n\t\t\t    jetton_amount = $2,\n\t\t    \tprocessed = not $4,\n\t\t    \texpired_at = $3,\n\t\t    \tfilled = $4\n\t\t\tWHERE  memo = $5\n\t\t`, tonAmount, t.JettonAmount, expiredAt, filled, t.Memo)\n\treturn err\n}\n\nfunc (c *Connection) IsWithdrawalRequestUnique(ctx context.Context, w core.WithdrawalRequest) (bool, error) {\n\tvar queryID int64\n\terr := c.client.QueryRow(ctx, `\n\t\tSELECT query_id\n\t\tFROM payments.withdrawal_requests\n\t\tWHERE user_id = $1 AND user_query_id = $2 AND is_internal = false \n\t`,\n\t\tw.UserID,\n\t\tw.QueryID,\n\t).Scan(&queryID)\n\tif errors.Is(err, pgx.ErrNoRows) {\n\t\treturn true, nil\n\t}\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn false, nil\n}\n\nfunc (c *Connection) GetExternalWithdrawalTasks(ctx context.Context, limit int) ([]core.ExternalWithdrawalTask, error) {\n\tvar res []core.ExternalWithdrawalTask\n\trows, err := c.client.Query(ctx, `\n\t\tSELECT DISTINCT ON (dest_address) dest_address,\n\t\t                                  query_id,\n\t\t                                  currency,\n\t\t                                  bounceable,\n\t\t                                  comment,\n\t\t                                  amount,\n\t\t                                  binary_comment\n\t\tFROM   payments.withdrawal_requests\n\t\tWHERE  processing = false\n        ORDER BY dest_address, query_id\n\t\tLIMIT  $1\n\t`, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar w core.ExternalWithdrawalTask\n\t\terr = rows.Scan(&w.Destination, &w.QueryID, &w.Currency, &w.Bounceable, &w.Comment, &w.Amount, &w.BinaryComment)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, w)\n\t}\n\tif rows.Err() != nil {\n\t\treturn nil, rows.Err()\n\t}\n\treturn res, nil\n}\n\n// GetServiceHotWithdrawalTasks return tasks for Hot wallet withdrawals\nfunc (c *Connection) GetServiceHotWithdrawalTasks(ctx context.Context, limit int) ([]core.ServiceWithdrawalTask, error) {\n\tvar tasks []core.ServiceWithdrawalTask\n\trows, err := c.client.Query(ctx, `\n\t\tSELECT DISTINCT ON (from_address) swr.from_address,\n\t\t                                  swr.memo,\n\t\t                                  swr.jetton_master,\n\t\t                                  tw.subwallet_id\n\t\tFROM   payments.service_withdrawal_requests swr\n\t\tLEFT JOIN payments.ton_wallets tw ON swr.from_address = tw.address\n\t\tWHERE  processed = false and type = ANY($1) and filled = false\n        ORDER BY from_address\n\t\tLIMIT  $2\n\t`, []core.WalletType{core.JettonOwner, core.TonDepositWallet}, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar w core.ServiceWithdrawalTask\n\t\terr = rows.Scan(&w.From, &w.Memo, &w.JettonMaster, &w.SubwalletID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttasks = append(tasks, w)\n\t}\n\tif rows.Err() != nil {\n\t\treturn nil, rows.Err()\n\t}\n\treturn tasks, nil\n}\n\n// GetServiceDepositWithdrawalTasks return tasks for TON deposit wallets\nfunc (c *Connection) GetServiceDepositWithdrawalTasks(ctx context.Context, limit int) ([]core.ServiceWithdrawalTask, error) {\n\tvar tasks []core.ServiceWithdrawalTask\n\trows, err := c.client.Query(ctx, `\n\t\tSELECT DISTINCT ON (from_address) swr.from_address,\n\t\t                                  swr.memo,\n\t\t                                  swr.jetton_master,\n\t\t                                  swr.jetton_amount,\n\t\t                                  tw.subwallet_id\n\t\tFROM   payments.service_withdrawal_requests swr\n\t\tLEFT JOIN payments.ton_wallets tw ON swr.from_address = tw.address\n\t\tWHERE  processed = false AND filled = true AND type = $1\n        ORDER BY from_address\n\t\tLIMIT $2\n\t`, core.TonDepositWallet, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar w core.ServiceWithdrawalTask\n\t\terr = rows.Scan(&w.From, &w.Memo, &w.JettonMaster, &w.JettonAmount, &w.SubwalletID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttasks = append(tasks, w)\n\t}\n\tif rows.Err() != nil {\n\t\treturn nil, rows.Err()\n\t}\n\treturn tasks, nil\n}\n\nfunc saveBlock(ctx context.Context, tx pgx.Tx, block core.ShardBlockHeader) error {\n\t_, err := tx.Exec(ctx, `\n\t\tINSERT INTO payments.block_data (\n\t\tshard,\n\t\tseqno,\n\t\troot_hash,\n\t\tfile_hash,\n\t\tgen_utime                                 \n\t\t) VALUES ($1, $2, $3, $4, $5)                                               \n\t`,\n\t\tblock.Shard,\n\t\tblock.SeqNo,\n\t\tblock.RootHash,\n\t\tblock.FileHash,\n\t\ttime.Unix(int64(block.GenUtime), 0),\n\t)\n\treturn err\n}\n\nfunc updateInternalWithdrawal(ctx context.Context, tx pgx.Tx, w core.InternalWithdrawal) error {\n\tmemo, err := uuid.FromString(w.Memo)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\tsendingLt     *int64\n\t\talreadyFailed bool\n\t)\n\n\terr = tx.QueryRow(ctx, `\n\t\tSELECT failed, sending_lt\n\t\tFROM payments.internal_withdrawals\n\t\tWHERE  memo = $1\n\t`, memo).Scan(&alreadyFailed, &sendingLt)\n\n\tif alreadyFailed {\n\t\taudit.Log(audit.Error, \"internal withdrawal message\", core.InternalWithdrawalEvent,\n\t\t\tfmt.Sprintf(\"successful withdrawal for expired internal withdrawal message. memo: %v\", w.Memo))\n\t\treturn fmt.Errorf(\"invalid behavior of the expiration processor\")\n\t}\n\n\tif sendingLt == nil {\n\t\taudit.Log(audit.Error, \"internal withdrawal message\", core.InternalWithdrawalEvent,\n\t\t\tfmt.Sprintf(\"successful withdrawal without sending confirmation. memo: %v\", w.Memo))\n\t\treturn fmt.Errorf(\"invalid event order\")\n\t}\n\n\tif w.IsFailed {\n\t\t_, err = tx.Exec(ctx, `\n\t\t\tUPDATE payments.internal_withdrawals\n\t\t\tSET\n\t\t    \tfailed = true\n\t\t\tWHERE  memo = $1\n\t\t`, memo)\n\t\treturn err\n\t}\n\t_, err = tx.Exec(ctx, `\n\t\t\tUPDATE payments.internal_withdrawals\n\t\t\tSET\n\t\t    \tfinish_lt = $1,\n\t\t    \tfinished_at = $2,\n\t\t    \tamount = amount + $3\n\t\t\tWHERE  memo = $4\n\t\t`, w.Lt, time.Unix(int64(w.Utime), 0), w.Amount, memo)\n\treturn err\n}\n\nfunc (c *Connection) SaveInternalWithdrawalTask(\n\tctx context.Context,\n\ttask core.InternalWithdrawalTask,\n\texpiredAt time.Time,\n\tmemo uuid.UUID,\n) error {\n\t_, err := c.client.Exec(ctx, `\n\t\tINSERT INTO payments.internal_withdrawals (\n\t\tsince_lt,\n\t\tfrom_address,\n\t\texpired_at,\n\t\tmemo\n\t\t) VALUES ($1, $2, $3, $4)\n\t`,\n\t\ttask.Lt,\n\t\ttask.From,\n\t\texpiredAt,\n\t\tmemo,\n\t)\n\treturn err\n}\n\nfunc (c *Connection) SaveParsedBlockData(ctx context.Context, events core.BlockEvents) error {\n\ttx, err := c.client.Begin(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback(ctx)\n\tfor _, ei := range events.ExternalIncomes {\n\t\terr = saveExternalIncome(ctx, tx, ei)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor _, ii := range events.InternalIncomes {\n\t\terr = c.saveInternalIncome(ctx, tx, ii)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor _, sc := range events.SendingConfirmations {\n\t\terr = applySendingConfirmations(ctx, tx, sc)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor _, iw := range events.InternalWithdrawals {\n\t\terr = updateInternalWithdrawal(ctx, tx, iw)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor _, ew := range events.ExternalWithdrawals {\n\t\terr = updateExternalWithdrawal(ctx, tx, ew)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor _, wc := range events.WithdrawalConfirmations {\n\t\terr = applyJettonWithdrawalConfirmation(ctx, tx, wc)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\terr = saveBlock(ctx, tx, events.Block)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = tx.Commit(ctx)\n\treturn err\n}\n\nfunc (c *Connection) GetTonInternalWithdrawalTasks(ctx context.Context, limit int) ([]core.InternalWithdrawalTask, error) {\n\tvar tasks []core.InternalWithdrawalTask\n\t// lt > finish_lt condition because all TONs withdraws\n\trows, err := c.client.Query(ctx, `\n\t\tSELECT deposit_address, MAX(lt) AS last_lt, tw.subwallet_id\n\t\tFROM payments.external_incomes di\n\t\t\tLEFT JOIN (\n\t\t\tSELECT iw1.from_address, iw1.since_lt, iw1.finish_lt\n\t\t\tFROM payments.internal_withdrawals iw1\n\t\t\tJOIN (\n\t\t\t    SELECT from_address, MAX(since_lt) AS max_since_lt\n\t\t\t    FROM payments.internal_withdrawals\n\t\t\t    WHERE failed = false \n\t\t\t    GROUP BY from_address \n\t\t\t) iw2 ON iw2.from_address = iw1.from_address AND iw2.max_since_lt = iw1.since_lt\n\t\t\tWHERE iw1.failed = false\n\t\t) as iw3 ON from_address = deposit_address\n\t\tJOIN payments.ton_wallets tw ON di.deposit_address = tw.address\n\t\tWHERE ((since_lt IS NOT NULL AND finish_lt IS NOT NULL AND lt > finish_lt) OR (since_lt IS NULL)) \n\t\t  AND type = $1\n\t\tGROUP BY deposit_address, tw.subwallet_id\n\t\tORDER BY MAX(di.amount) DESC\n\t\tLIMIT $2\n\t`, core.TonDepositWallet, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar task core.InternalWithdrawalTask\n\t\terr = rows.Scan(&task.From, &task.Lt, &task.SubwalletID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttask.Currency = core.TonSymbol\n\t\ttasks = append(tasks, task)\n\t}\n\tif rows.Err() != nil {\n\t\treturn nil, rows.Err()\n\t}\n\treturn tasks, nil\n}\n\nfunc (c *Connection) GetJettonInternalWithdrawalTasks(\n\tctx context.Context,\n\tforbiddenAddresses []core.Address,\n\tlimit int,\n) (\n\t[]core.InternalWithdrawalTask, error,\n) {\n\tvar tasks []core.InternalWithdrawalTask\n\texcludedAddr := make([][]byte, 0) // it is important for 'deposit_address = ANY($2)' sql constraint\n\tfor _, a := range forbiddenAddresses {\n\t\texcludedAddr = append(excludedAddr, a[:]) // array of core.Address not supported by driver\n\t}\n\trows, err := c.client.Query(ctx, `\n\t\tSELECT deposit_address, MAX(lt) AS last_lt, jw.subwallet_id, jw.currency\n\t\tFROM payments.external_incomes di\n\t\t\tLEFT JOIN (\n\t\t\tSELECT iw1.from_address, iw1.since_lt, iw1.finish_lt\n\t\t\tFROM payments.internal_withdrawals iw1\n\t\t\tJOIN (\n\t\t\t    SELECT from_address, MAX(since_lt) AS max_since_lt\n\t\t\t    FROM payments.internal_withdrawals\n\t\t\t    WHERE failed = false \n\t\t\t    GROUP BY from_address \n\t\t\t) iw2 ON iw2.from_address = iw1.from_address AND iw2.max_since_lt = iw1.since_lt\n\t\t\tWHERE iw1.failed = false\n\t\t) as iw3 ON from_address = deposit_address\n\t\tJOIN payments.jetton_wallets jw ON di.deposit_address = jw.address\n\t\tLEFT JOIN payments.ton_wallets tw ON jw.subwallet_id = tw.subwallet_id\n\t\tWHERE ((since_lt IS NOT NULL AND lt > since_lt AND finish_lt IS NOT NULL)\n\t\t   OR (since_lt IS NULL)) AND jw.type = $1 AND NOT tw.address = ANY($2)\n\t\tGROUP BY deposit_address, jw.subwallet_id, jw.currency\n\t\tORDER BY MAX(di.amount) DESC\n\t\tLIMIT $3\n\t`, core.JettonDepositWallet, excludedAddr, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar task core.InternalWithdrawalTask\n\t\terr = rows.Scan(&task.From, &task.Lt, &task.SubwalletID, &task.Currency)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttasks = append(tasks, task)\n\t}\n\tif rows.Err() != nil {\n\t\treturn nil, rows.Err()\n\t}\n\treturn tasks, nil\n}\n\nfunc applyJettonWithdrawalConfirmation(\n\tctx context.Context,\n\ttx pgx.Tx,\n\tconfirm core.JettonWithdrawalConfirmation,\n) error {\n\t_, err := tx.Exec(ctx, `\n\t\t\tUPDATE payments.external_withdrawals\n\t\t\tSET\n\t\t    \tconfirmed = true\n\t\t\tWHERE  query_id = $1 AND processed_lt IS NOT NULL\n\t\t`, confirm.QueryId)\n\treturn err\n}\n\nfunc updateExternalWithdrawal(ctx context.Context, tx pgx.Tx, w core.ExternalWithdrawal) error {\n\tvar queryID int64\n\n\tvar alreadyFailed bool\n\terr := tx.QueryRow(ctx, `\n\t\tSELECT failed\n\t\tFROM payments.external_withdrawals\n\t\tWHERE  msg_uuid = $1 AND address = $2\n\t`, w.ExtMsgUuid, w.To).Scan(&alreadyFailed)\n\tif alreadyFailed {\n\t\taudit.Log(audit.Error, \"external withdrawal message\", core.ExternalWithdrawalEvent,\n\t\t\tfmt.Sprintf(\"successful withdrawal for expired external withdrawal message. msg uuid: %v\", w.ExtMsgUuid.String()))\n\t\treturn fmt.Errorf(\"invalid behavior of the expiration processor\")\n\t}\n\n\t// if there was a transaction on the hot wallet but the message was not sent\n\t// set processed = true to prevent burn balance\n\tif w.IsFailed {\n\t\terr := tx.QueryRow(ctx, `\n\t\t\tUPDATE payments.external_withdrawals\n\t\t\tSET\n\t\t    \tfailed = true,\n\t\t    \ttx_hash = $1\n\t\t\tWHERE  msg_uuid = $2 AND address = $3\n\t\t\tRETURNING query_id\n\t\t`, w.TxHash, w.ExtMsgUuid, w.To).Scan(&queryID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = tx.Exec(ctx, `\n\t\t\tUPDATE payments.withdrawal_requests\n\t\t\tSET\n\t\t    \tprocessed = true\n\t\t\tWHERE  query_id = $1\n\t\t`, queryID)\n\t\treturn err\n\t}\n\n\terr = tx.QueryRow(ctx, `\n\t\t\tUPDATE payments.external_withdrawals\n\t\t\tSET\n\t\t    \tprocessed_lt = $1,\n\t\t    \tprocessed_at = $2,\n\t\t    \ttx_hash = $3\n\t\t\tWHERE  msg_uuid = $4 AND address = $5\n\t\t\tRETURNING query_id\n\t\t`, w.Lt, time.Unix(int64(w.Utime), 0), w.TxHash, w.ExtMsgUuid, w.To).Scan(&queryID)\n\tif errors.Is(err, pgx.ErrNoRows) {\n\t\taudit.Log(audit.Error, \"external withdrawal message\", core.ExternalWithdrawalEvent,\n\t\t\tfmt.Sprintf(\"successful withdrawal not linked to any known withdrawal request; possibly a manual hot wallet withdrawal. tx hash: %x\", w.TxHash))\n\t\treturn fmt.Errorf(\"anomalous behavior of the TON hot wallet\")\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = tx.Exec(ctx, `\n\t\t\tUPDATE payments.withdrawal_requests\n\t\t\tSET\n\t\t    \tprocessed = true\n\t\t\tWHERE  query_id = $1\n\t\t`, queryID)\n\treturn err\n}\n\nfunc applySendingConfirmations(ctx context.Context, tx pgx.Tx, w core.SendingConfirmation) error {\n\tvar alreadyFailed bool\n\tmemo, err := uuid.FromString(w.Memo)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = tx.QueryRow(ctx, `\n\t\t\tUPDATE payments.internal_withdrawals\n\t\t\tSET\n\t\t    \tsending_lt = $1\n\t\t\tWHERE  memo = $2\n\t\t\tRETURNING failed\n\t\t`, w.Lt, memo).Scan(&alreadyFailed)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif alreadyFailed {\n\t\taudit.Log(audit.Error, \"internal withdrawal message\", core.InternalWithdrawalEvent,\n\t\t\tfmt.Sprintf(\"successful sending for expired internal withdrawal message. memo: %v\", w.Memo))\n\t\treturn fmt.Errorf(\"invalid behavior of the expiration processor\")\n\t}\n\treturn err\n}\n\nfunc (c *Connection) CreateExternalWithdrawals(\n\tctx context.Context,\n\ttasks []core.ExternalWithdrawalTask,\n\textMsgUuid uuid.UUID,\n\texpiredAt time.Time,\n) error {\n\ttx, err := c.client.Begin(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback(ctx)\n\n\tfor _, t := range tasks {\n\t\t_, err = tx.Exec(ctx, `\n\t\t\tINSERT INTO payments.external_withdrawals (\n\t\t\t\tmsg_uuid,\n\t\t\t\tquery_id,\n\t\t\t\texpired_at,\n\t\t\t\taddress\n\t\t\t) VALUES ($1, $2, $3, $4)                                               \n\t\t`, extMsgUuid, t.QueryID, expiredAt, t.Destination)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = tx.Exec(ctx, `\n\t\t\tUPDATE payments.withdrawal_requests\n\t\t\tSET\n\t\t    \tprocessing = true\t\t    \t\n\t\t\tWHERE  query_id = $1\n\t\t`, t.QueryID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn tx.Commit(ctx)\n}\n\nfunc (c *Connection) GetTonHotWalletAddress(ctx context.Context) (core.Address, error) {\n\tvar addr core.Address\n\terr := c.client.QueryRow(ctx, `\n\t\tSELECT address \n\t\tFROM payments.ton_wallets\n\t\tWHERE TYPE = $1\n\t`, core.TonHotWallet).Scan(&addr)\n\tif errors.Is(err, pgx.ErrNoRows) {\n\t\terr = core.ErrNotFound\n\t}\n\treturn addr, err\n}\n\nfunc (c *Connection) GetLastSavedBlockID(ctx context.Context) (*ton.BlockIDExt, error) {\n\tvar blockID ton.BlockIDExt\n\terr := c.client.QueryRow(ctx, `\n\t\tSELECT \n\t\t    seqno, \n\t\t    shard, \n\t\t    root_hash, \n\t\t    file_hash\n\t\tFROM payments.block_data\n\t\tORDER BY seqno DESC\n\t\tLIMIT 1\n\t`).Scan(\n\t\t&blockID.SeqNo,\n\t\t&blockID.Shard,\n\t\t&blockID.RootHash,\n\t\t&blockID.FileHash,\n\t)\n\tif errors.Is(err, pgx.ErrNoRows) {\n\t\treturn nil, core.ErrNotFound\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tblockID.Workchain = core.DefaultWorkchain\n\treturn &blockID, nil\n}\n\n// SetExpired TODO: maybe add block related expiration\nfunc (c *Connection) SetExpired(ctx context.Context) error {\n\t_, err := c.client.Exec(ctx, `\n\t\t\tUPDATE payments.internal_withdrawals\n\t\t\tSET\n\t\t    \tfailed = true\t\t    \t\n\t\t\tWHERE  expired_at < $1 AND sending_lt IS NULL AND failed = false\n\t`, time.Now().Add(-config.AllowableBlockchainLagging))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttx, err := c.client.Begin(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback(ctx)\n\n\t// processed_lt IS NULL AND failed = false - for lost external messages\n\trows, err := tx.Query(ctx, `\n\t\t\tUPDATE payments.external_withdrawals\n\t\t\tSET\n\t\t    \tfailed = true\t\t    \t\n\t\t\tWHERE  expired_at < $1 AND processed_lt IS NULL AND failed = false\n\t\t\tRETURNING query_id\n\t`, time.Now().Add(-config.AllowableBlockchainLagging))\n\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\n\tvar ids []int64\n\tfor rows.Next() {\n\t\tvar queryID int64\n\t\terr = rows.Scan(&queryID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tids = append(ids, queryID)\n\t}\n\tif rows.Err() != nil {\n\t\treturn rows.Err()\n\t}\n\n\tfor _, id := range ids {\n\t\t_, err = tx.Exec(ctx, `\n\t\t\tUPDATE payments.withdrawal_requests\n\t\t\tSET\n\t\t    \tprocessing = false\n\t\t\tWHERE  query_id = $1\n\t\t`, id)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn tx.Commit(ctx)\n}\n\nfunc (c *Connection) IsActualBlockData(ctx context.Context) (bool, int64, error) {\n\tvar lastBlockTime time.Time\n\terr := c.client.QueryRow(ctx, `\n\t\tSELECT \n\t\t    gen_utime\n\t\tFROM payments.block_data\n\t\tORDER BY seqno DESC\n\t\tLIMIT 1\n\t`).Scan(&lastBlockTime)\n\tif errors.Is(err, pgx.ErrNoRows) {\n\t\treturn false, 0, nil\n\t}\n\tif err != nil {\n\t\treturn false, 0, err\n\t}\n\treturn time.Since(lastBlockTime) < config.AllowableBlockchainLagging, lastBlockTime.Unix(), nil\n}\n\nfunc (c *Connection) IsInProgressInternalWithdrawalRequest(\n\tctx context.Context,\n\tdest core.Address,\n\tcurrency string,\n) (\n\tbool,\n\terror,\n) {\n\tvar queryID int64\n\terr := c.client.QueryRow(ctx, `\n\t\tSELECT query_id\n\t\tFROM payments.withdrawal_requests\n\t\tWHERE dest_address = $1 AND \n\t\t      currency = $2 AND \n\t\t      is_internal = true AND\n\t\t      processed = false\n\t\tLIMIT 1\n\t`,\n\t\tdest,\n\t\tcurrency,\n\t).Scan(&queryID)\n\tif errors.Is(err, pgx.ErrNoRows) {\n\t\treturn false, nil\n\t}\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetExternalWithdrawalStatus returns status and hash of transaction for external withdrawal\nfunc (c *Connection) GetExternalWithdrawalStatus(ctx context.Context, id int64) (core.WithdrawalData, error) {\n\tvar (\n\t\tprocessing, processed bool\n\t\tdata                  core.WithdrawalData\n\t)\n\terr := c.client.QueryRow(ctx, `\n\t\tSELECT processing, processed, user_id, user_query_id\n\t\tFROM payments.withdrawal_requests\n\t\tWHERE query_id = $1 AND is_internal = false\n\t\tLIMIT 1\n\t`, id).Scan(&processing, &processed, &data.UserID, &data.QueryID)\n\tif errors.Is(err, pgx.ErrNoRows) {\n\t\treturn core.WithdrawalData{}, core.ErrNotFound\n\t}\n\tif err != nil {\n\t\treturn core.WithdrawalData{}, err\n\t}\n\tif processing && processed {\n\t\tvar (\n\t\t\ttxHash   []byte\n\t\t\tisFailed bool\n\t\t)\n\t\t// must be only one record\n\t\t// OR processed_lt IS NOT NULL for DB up to 0.5.0 version\n\t\terr = c.client.QueryRow(ctx, `\n\t\tSELECT tx_hash, failed\n\t\tFROM payments.external_withdrawals\n\t\tWHERE query_id = $1 AND (tx_hash IS NOT NULL OR processed_lt IS NOT NULL)\n\t\tLIMIT 1\n\t`, id).Scan(&txHash, &isFailed)\n\t\tif err != nil {\n\t\t\treturn core.WithdrawalData{}, err\n\t\t}\n\t\tif isFailed {\n\t\t\tdata.Status = core.FailedStatus\n\t\t\tdata.TxHash = txHash\n\t\t\treturn data, nil\n\t\t}\n\t\tdata.Status = core.ProcessedStatus\n\t\tdata.TxHash = txHash\n\t\treturn data, nil\n\t} else if processing && !processed {\n\t\tdata.Status = core.ProcessingStatus\n\t\treturn data, nil\n\t} else if !processing && !processed {\n\t\tdata.Status = core.PendingStatus\n\t\treturn data, nil\n\t}\n\treturn core.WithdrawalData{}, fmt.Errorf(\"bad status\")\n}\n\n// GetIncome returns list of incomes by user_id\nfunc (c *Connection) GetIncome(\n\tctx context.Context,\n\tuserID string,\n\tisDepositSide bool,\n) (\n\t[]core.TotalIncome,\n\terror,\n) {\n\tvar sqlStatement string\n\tif isDepositSide {\n\t\tsqlStatement = `\n\t\t\tSELECT COALESCE(jw.address,tw.address) as deposit, COALESCE(SUM(i.amount),0) as balance, COALESCE(jw.currency,$1) as currency\n\t\t\tFROM payments.ton_wallets tw\n         \t\tLEFT JOIN payments.jetton_wallets jw ON jw.subwallet_id = tw.subwallet_id\n         \t\tLEFT JOIN payments.external_incomes i ON i.deposit_address = COALESCE(jw.address,tw.address)\n\t\t\tWHERE tw.user_id = $2 AND tw.type = ANY($3)\n\t\t\tGROUP BY deposit, tw.address, jw.currency\n\t\t`\n\t} else {\n\t\tsqlStatement = `\n\t\t\tSELECT COALESCE(jw.address,tw.address) as deposit, COALESCE(SUM(i.amount),0) as balance, COALESCE(jw.currency,$1) as currency\n\t\t\tFROM payments.ton_wallets tw\n         \t\tLEFT JOIN payments.jetton_wallets jw ON jw.subwallet_id = tw.subwallet_id\n         \t\tLEFT JOIN payments.internal_incomes i ON i.deposit_address = COALESCE(jw.address,tw.address)\n\t\t\tWHERE tw.user_id = $2 AND tw.type = ANY($3)\n\t\t\tGROUP BY deposit, tw.address, jw.currency\n\t\t`\n\t}\n\n\trows, err := c.client.Query(\n\t\tctx,\n\t\tsqlStatement,\n\t\tcore.TonSymbol,\n\t\tuserID,\n\t\t[]core.WalletType{core.TonDepositWallet, core.JettonOwner},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tres := make([]core.TotalIncome, 0)\n\tfor rows.Next() {\n\t\tvar deposit core.TotalIncome\n\t\terr = rows.Scan(&deposit.Deposit, &deposit.Amount, &deposit.Currency)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, deposit)\n\t}\n\tif rows.Err() != nil {\n\t\treturn nil, rows.Err()\n\t}\n\treturn res, nil\n}\n\n// GetIncomeHistory returns list of external incomes for deposit side by user_id and currency\nfunc (c *Connection) GetIncomeHistory(\n\tctx context.Context,\n\tuserID string,\n\tcurrency string,\n\tlimit int,\n\toffset int,\n\tascOrder bool,\n) (\n\t[]core.ExternalIncome,\n\terror,\n) {\n\tvar (\n\t\tres          []core.ExternalIncome\n\t\tsqlStatement string\n\t\twalletType   core.WalletType\n\t)\n\n\torder := \"DESC\"\n\tif ascOrder {\n\t\torder = \"ASC\"\n\t}\n\n\tif currency == core.TonSymbol {\n\t\tsqlStatement = fmt.Sprintf(`\n\t\t\tSELECT utime, lt, payer_address, deposit_address, amount, comment, payer_workchain, tx_hash\n\t\t\tFROM payments.external_incomes i\n\t\t\t\tLEFT JOIN payments.ton_wallets tw ON i.deposit_address = tw.address\n\t\t\tWHERE tw.type = $1 AND tw.user_id = $2 AND $3 = $3\n\t\t\tORDER BY lt %s\n\t\t\tLIMIT $4\n\t\t\tOFFSET $5\n\t\t`, order)\n\t\twalletType = core.TonDepositWallet\n\t} else {\n\t\tsqlStatement = fmt.Sprintf(`\n\t\t\tSELECT utime, lt, payer_address, deposit_address, amount, comment, payer_workchain, tx_hash\n\t\t\tFROM payments.external_incomes i\n\t\t\t    LEFT JOIN payments.jetton_wallets jw ON i.deposit_address = jw.address\n\t\t\tWHERE jw.type = $1 AND jw.user_id = $2 AND jw.currency = $3\n\t\t\tORDER BY lt %s\n\t\t\tLIMIT $4\n\t\t\tOFFSET $5\n\t\t`, order)\n\t\twalletType = core.JettonDepositWallet\n\t}\n\n\trows, err := c.client.Query(ctx, sqlStatement, walletType, userID, currency, limit, offset)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar (\n\t\t\tincome core.ExternalIncome\n\t\t\tt      time.Time\n\t\t)\n\t\terr = rows.Scan(&t, &income.Lt, &income.From, &income.To, &income.Amount, &income.Comment, &income.FromWorkchain, &income.TxHash)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tincome.Utime = uint32(t.Unix())\n\t\tres = append(res, income)\n\t}\n\tif rows.Err() != nil {\n\t\treturn nil, rows.Err()\n\t}\n\treturn res, nil\n}\n\n// GetIncomeByTx returns external income and currency for deposit side by transaction hash\nfunc (c *Connection) GetIncomeByTx(\n\tctx context.Context,\n\ttxHash []byte,\n) (\n\t*core.ExternalIncome,\n\tstring,\n\terror,\n) {\n\n\tvar (\n\t\tincome core.ExternalIncome\n\t\tt      time.Time\n\t)\n\n\terr := c.client.QueryRow(ctx, `\n\t\tSELECT utime, lt, payer_address, deposit_address, amount, comment, payer_workchain\n\t\tFROM payments.external_incomes i\n\t\tLEFT JOIN payments.ton_wallets tw ON i.deposit_address = tw.address\n\t\tWHERE tw.type = $1 AND i.tx_hash = $2\n\t\tLIMIT 1\n\t`, core.TonDepositWallet, txHash).Scan(&t, &income.Lt, &income.From, &income.To, &income.Amount, &income.Comment, &income.FromWorkchain)\n\tif errors.Is(err, pgx.ErrNoRows) {\n\t\tvar currency string\n\t\t// amount > 0 means receiving an aggregated transaction for an unidentified jetton replenishment\n\t\terr = c.client.QueryRow(ctx, `\n\t\t\tSELECT utime, lt, payer_address, deposit_address, amount, comment, payer_workchain, jw.currency\n\t\t\tFROM payments.external_incomes i\n\t\t\tLEFT JOIN payments.jetton_wallets jw ON i.deposit_address = jw.address\n\t\t\tWHERE jw.type = $1 AND i.tx_hash = $2 AND i.amount > 0\n\t\t\tLIMIT 1\n\t\t`, core.JettonDepositWallet, txHash).Scan(&t, &income.Lt, &income.From, &income.To, &income.Amount, &income.Comment, &income.FromWorkchain, &currency)\n\t\tif errors.Is(err, pgx.ErrNoRows) {\n\t\t\treturn nil, \"\", core.ErrNotFound // not found\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, \"\", err\n\t\t}\n\t\tincome.Utime = uint32(t.Unix())\n\t\tincome.TxHash = txHash\n\t\treturn &income, currency, nil\n\t}\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tincome.Utime = uint32(t.Unix())\n\tincome.TxHash = txHash\n\treturn &income, core.TonSymbol, nil\n}\n\nfunc (c *Connection) GetTotalWithdrawalAmounts(ctx context.Context, currency string) (*core.TotalWithdrawalsAmount, error) {\n\n\ttx, err := c.client.Begin(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer tx.Rollback(ctx)\n\n\tvar totalAmounts core.TotalWithdrawalsAmount\n\n\terr = tx.QueryRow(ctx, `\n\t\tSELECT \n\t\t    COALESCE(SUM(amount), 0 ) as total_processing_amount\n\t\tFROM payments.withdrawal_requests\n\t\tWHERE currency = $1 AND processed = false AND processing = true \n\t`, currency).Scan(\n\t\t&totalAmounts.Processing,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = tx.QueryRow(ctx, `\n\t\tSELECT \n\t\t    COALESCE(SUM(amount), 0 ) as total_pending_amount\n\t\tFROM payments.withdrawal_requests\n\t\tWHERE currency = $1 AND processed = false AND processing = false \n\t`, currency).Scan(\n\t\t&totalAmounts.Pending,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = tx.Commit(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &totalAmounts, nil\n\n}\n"
  },
  {
    "path": "db/db_test.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"github.com/gobicycle/bicycle/core\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar dbURI string\n\nfunc execMultiStatement(c *Connection, ctx context.Context, query string) error {\n\tquery = strings.TrimPrefix(query, \"BEGIN;\")\n\tquery = strings.TrimSuffix(query, \"COMMIT;\")\n\tqueries := strings.Split(query, \";\")\n\n\ttx, err := c.client.Begin(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback(ctx)\n\tfor _, q := range queries {\n\t\t_, err := tx.Exec(ctx, q)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\terr = tx.Commit(ctx)\n\treturn err\n}\n\nfunc migrateUp(c *Connection, t *testing.T, source string) error {\n\tmigrateDown(c, t)\n\tdeploy, err := os.ReadFile(\"../deploy/db/01_init.up.sql\")\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = execMultiStatement(c, context.Background(), string(deploy))\n\tif err != nil {\n\t\treturn err\n\t}\n\ttest, err := os.ReadFile(\"tests/\" + source + \"/01_data.up.sql\")\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = execMultiStatement(c, context.Background(), string(test))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc migrateDown(c *Connection, t *testing.T) {\n\tdrop, err := os.ReadFile(\"../deploy/db/01_init.down.sql\")\n\tif err != nil {\n\t\tt.Fatal(\"migrate down err: \", err)\n\t}\n\terr = execMultiStatement(c, context.Background(), string(drop))\n\tif err != nil {\n\t\tt.Fatal(\"migrate down err: \", err)\n\t}\n}\n\nfunc init() {\n\tdbURI = os.Getenv(\"DB_URI\")\n\tif dbURI == \"\" {\n\t\tpanic(\"empty db uri var\")\n\t}\n}\n\nfunc connect(t *testing.T) *Connection {\n\tc, err := NewConnection(dbURI)\n\tif err != nil {\n\t\tt.Fatal(\"connections err: \", err)\n\t}\n\treturn c\n}\n\nfunc Test_NewConnection(t *testing.T) {\n\tconnect(t)\n}\n\nfunc Test_GetTonInternalWithdrawalTasks(t *testing.T) {\n\tc := connect(t)\n\tsource := \"get-ton-internal-withdrawal-tasks\"\n\terr := migrateUp(c, t, source)\n\tif err != nil {\n\t\tt.Fatal(\"migrate up err: \", err)\n\t}\n\tdefer migrateDown(c, t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*2)\n\tdefer cancel()\n\tres, err := c.GetTonInternalWithdrawalTasks(ctx, 100)\n\tif err != nil {\n\t\tt.Fatal(\"get tasks err: \", err)\n\t}\n\tif len(res) != 1 {\n\t\tt.Fatal(\"only one task must be loaded\")\n\t}\n\tif res[0].SubwalletID != 2 {\n\t\tt.Fatal(\"task must be loaded only for deposit A\")\n\t}\n\tif res[0].Lt != 3 {\n\t\tt.Fatal(\"task must be loaded only for second payment\")\n\t}\n}\n\nfunc Test_GetJettonInternalWithdrawalTasks(t *testing.T) {\n\tc := connect(t)\n\tsource := \"get-jetton-internal-withdrawal-tasks\"\n\terr := migrateUp(c, t, source)\n\tif err != nil {\n\t\tt.Fatal(\"migrate up err: \", err)\n\t}\n\tdefer migrateDown(c, t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*2)\n\tdefer cancel()\n\tres, err := c.GetJettonInternalWithdrawalTasks(ctx, []core.Address{}, 250)\n\tif err != nil {\n\t\tt.Fatal(\"get tasks err: \", err)\n\t}\n\tif len(res) != 2 {\n\t\tt.Fatal(\"two tasks must be loaded\")\n\t}\n\tif res[0].SubwalletID != 2 || res[1].SubwalletID != 4 {\n\t\tt.Fatal(\"tasks must be loaded only for deposits A and C\")\n\t}\n\tif res[0].Lt != 2 {\n\t\tt.Fatal(\"task must be loaded only for second payment for deposit A\")\n\t}\n\tif res[1].Lt != 1 {\n\t\tt.Fatal(\"task must be loaded only for first payment for deposit C\")\n\t}\n}\n\nfunc Test_GetJettonInternalWithdrawalTasksForbidden(t *testing.T) {\n\tc := connect(t)\n\tsource := \"get-jetton-internal-withdrawal-tasks\"\n\terr := migrateUp(c, t, source)\n\tif err != nil {\n\t\tt.Fatal(\"migrate up err: \", err)\n\t}\n\tdefer migrateDown(c, t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*2)\n\tdefer cancel()\n\tb, _ := hex.DecodeString(\"01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab3\") // owner of Jetton deposit A\n\tvar forbiddenAddress core.Address\n\tcopy(forbiddenAddress[:], b)\n\tres, err := c.GetJettonInternalWithdrawalTasks(ctx, []core.Address{forbiddenAddress}, 250)\n\tif err != nil {\n\t\tt.Fatal(\"get tasks err: \", err)\n\t}\n\tif len(res) != 1 {\n\t\tt.Fatal(\"one tasks must be loaded\")\n\t}\n\tif res[0].SubwalletID != 4 {\n\t\tt.Fatal(\"tasks must be loaded only for deposits C\")\n\t}\n\tif res[0].Lt != 1 {\n\t\tt.Fatal(\"task must be loaded only for first payment for deposit C\")\n\t}\n}\n\nfunc Test_SetExpired(t *testing.T) {\n\ttype extResult struct {\n\t\tqueryID    int\n\t\tfailed     bool\n\t\tprocessing bool\n\t\tprocessed  bool\n\t}\n\texternalResult := [7]extResult{\n\t\t{1, true, false, false},\n\t\t{2, true, false, false},\n\t\t{3, true, false, false},\n\t\t{4, false, true, false},\n\t\t{5, true, false, false},\n\t\t{6, false, true, true},\n\t\t{7, false, true, true},\n\t}\n\tinternalResult := [6]bool{true, true, false, true, false, false}\n\n\tc := connect(t)\n\tsource := \"set-expired\"\n\terr := migrateUp(c, t, source)\n\tif err != nil {\n\t\tt.Fatal(\"migrate up err: \", err)\n\t}\n\tdefer migrateDown(c, t)\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*20)\n\tdefer cancel()\n\terr = c.SetExpired(ctx)\n\tif err != nil {\n\t\tt.Fatal(\"set expired err: \", err)\n\t}\n\n\t// load external withdrawals data\n\trows, err := c.client.Query(ctx, `\n\t\tSELECT ew.query_id, failed, wr.processing, wr.processed\n\t\tFROM   payments.external_withdrawals ew\n\t\tLEFT JOIN payments.withdrawal_requests wr ON wr.query_id = ew.query_id \n        ORDER BY ew.query_id\n\t`)\n\tif err != nil {\n\t\tt.Fatal(\"get data err: \", err)\n\t}\n\tdefer rows.Close()\n\n\tvar (\n\t\textRes [7]extResult\n\t\ti      = 0\n\t)\n\tfor rows.Next() {\n\t\tvar r extResult\n\t\tvar err = rows.Scan(&r.queryID, &r.failed, &r.processing, &r.processed)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"scan err: \", err)\n\t\t}\n\t\textRes[i] = r\n\t\ti++\n\t}\n\tif rows.Err() != nil {\n\t\tt.Fatal(\"rows err: \", rows.Err())\n\t}\n\tif externalResult != extRes {\n\t\tt.Fatalf(\"invalid external result pattern: %v\", extRes)\n\t}\n\n\t// load internal withdrawals data\n\trows, err = c.client.Query(ctx, `\n\t\tSELECT failed\n\t\tFROM   payments.internal_withdrawals\n        ORDER BY since_lt\n\t`)\n\tif err != nil {\n\t\tt.Fatal(\"get data err: \", err)\n\t}\n\tdefer rows.Close()\n\tvar intRes [6]bool\n\ti = 0\n\tfor rows.Next() {\n\t\tvar r bool\n\t\tvar err = rows.Scan(&r)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"scan err: \", err)\n\t\t}\n\t\tintRes[i] = r\n\t\ti++\n\t}\n\tif rows.Err() != nil {\n\t\tt.Fatal(\"rows err: \", rows.Err())\n\t}\n\tif internalResult != intRes {\n\t\tt.Fatalf(\"invalid internal result pattern: %v\", intRes)\n\t}\n}\n\nfunc TestStripInvalidUTF8(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tinput:    \"hello world\",\n\t\t\texpected: \"hello world\",\n\t\t},\n\t\t{\n\t\t\tinput:    \"привет мир\",\n\t\t\texpected: \"привет мир\",\n\t\t},\n\t\t{\n\t\t\tinput:    string([]byte{'h', 'e', 'l', 'l', 0xb3, 'o'}),\n\t\t\texpected: \"hello\",\n\t\t},\n\t\t{\n\t\t\tinput:    string([]byte{0xff, 0xfe, 0xfd}),\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tinput:    \"valid \\xf0\\x9f\\x98\\x81 invalid \\xff text\",\n\t\t\texpected: \"valid 😁 invalid  text\",\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\tresult := stripInvalidUTF8(tt.input)\n\t\tif result != tt.expected {\n\t\t\tt.Errorf(\"test %d failed: expected %q, got %q\", i, tt.expected, result)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "db/tests/get-jetton-internal-withdrawal-tasks/01_data.up.sql",
    "content": "BEGIN;\n\nINSERT INTO payments.external_incomes (\n    lt,\n    utime,\n    deposit_address,\n    payer_address,\n    amount,\n    comment\n) VALUES\n    (\n        --      first payment to TON deposit A\n        1,\n        '2021-03-10 08:10:00 UTC',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab4', 'hex'),\n        123,\n        'test_comment_1'\n    ),\n    (\n        --      second payment to TON deposit A\n        2,\n        '2021-03-10 08:11:00 UTC',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab5', 'hex'),\n        123,\n        'test_comment_2'\n    ),\n    (\n        --      first payment to TON deposit B\n        1,\n        '2021-03-10 08:12:00 UTC',\n        decode('01bb00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab6', 'hex'),\n        123,\n        'test_comment_3'\n    ),\n    (\n        --      first payment to Jetton deposit C\n        1,\n        '2021-03-10 08:13:00 UTC',\n        decode('01cc00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab7', 'hex'),\n        123,\n        'test_comment_4'\n    );\n\nINSERT INTO payments.internal_withdrawals (\n    failed,\n    since_lt,\n    finish_lt,\n    created_at,\n    finished_at,\n    expired_at,\n    amount,\n    from_address,\n    memo\n) VALUES\n    (\n        --      finished withdrawal from TON deposit A after first payment\n        false,\n        1,\n        2,\n        '2021-03-10 08:14:00 UTC',\n        '2021-03-10 08:15:00 UTC',\n        '2021-03-10 08:17:00 UTC',\n        100,\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7831'\n    ),\n    (\n        --      failed withdrawal from TON deposit A after second payment\n        true,\n        2,\n        NULL,\n        '2021-03-10 08:16:00 UTC',\n        NULL,\n        '2021-03-10 08:19:00 UTC',\n        100,\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7832'\n    ),\n    (\n        --      not finished withdrawal from TON deposit B after payment\n        false,\n        1,\n        NULL,\n        '2021-03-10 08:14:00 UTC',\n        NULL,\n        '2021-03-10 08:17:00 UTC',\n        100,\n        decode('01bb00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7833'\n    );\n\nINSERT INTO payments.jetton_wallets (\n    subwallet_id,\n    created_at,\n    user_id,\n    currency,\n    type,\n    address\n) VALUES\n    (\n        --      Jetton hot wallet currency A\n        1,\n        '2021-03-10 08:00:00 UTC',\n        '',\n        'A',\n        'jetton_hot',\n        decode('00aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex')\n    ),\n    (\n        --      Jetton deposit A currency A\n        2,\n        '2021-03-10 08:00:00 UTC',\n        'test_user',\n        'A',\n        'jetton_deposit',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex')\n    ),\n    (\n        --      Jetton deposit B currency B\n        3,\n        '2021-03-10 08:00:00 UTC',\n        'test_user',\n        'B',\n        'jetton_deposit',\n        decode('01bb00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex')\n    ),\n    (\n        --      Jetton deposit C currency C\n        4,\n        '2021-03-10 08:00:00 UTC',\n        'test_user',\n        'C',\n        'jetton_deposit',\n        decode('01cc00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex')\n    );\n\nINSERT INTO payments.ton_wallets (\n    subwallet_id,\n    created_at,\n    user_id,\n    type,\n    address\n) VALUES\n      (\n          --      TON hot wallet\n          1,\n          '2021-03-10 08:00:00 UTC',\n          '',\n          'ton_hot',\n          decode('00aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab3', 'hex')\n      ),\n      (\n          --      Jetton deposit A owner\n          2,\n          '2021-03-10 08:00:00 UTC',\n          'test_user',\n          'owner',\n          decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab3', 'hex')\n      ),\n      (\n          --      Jetton deposit B owner\n          3,\n          '2021-03-10 08:00:00 UTC',\n          'test_user',\n          'owner',\n          decode('01bb00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab3', 'hex')\n      ),\n      (\n          --      Jetton deposit C owner\n          4,\n          '2021-03-10 08:00:00 UTC',\n          'test_user',\n          'owner',\n          decode('01cc00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab3', 'hex')\n      );\n\nCOMMIT;"
  },
  {
    "path": "db/tests/get-ton-internal-withdrawal-tasks/01_data.up.sql",
    "content": "BEGIN;\n\nINSERT INTO payments.external_incomes (\n    lt,\n    utime,\n    deposit_address,\n    payer_address,\n    amount,\n    comment\n) VALUES\n    (\n        --      first payment to TON deposit A\n        1,\n        '2021-03-10 08:10:00 UTC',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab4', 'hex'),\n        123,\n        'test_comment_1'\n    ),\n    (\n        --      second payment to TON deposit A\n        3,\n        '2021-03-10 08:11:00 UTC',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab5', 'hex'),\n        123,\n        'test_comment_2'\n    ),\n    (\n        --      first payment to TON deposit B\n        1,\n        '2021-03-10 08:12:00 UTC',\n        decode('01bb00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab6', 'hex'),\n        123,\n        'test_comment_3'\n    ),\n    (\n        --      first payment to Jetton deposit C\n        1,\n        '2021-03-10 08:13:00 UTC',\n        decode('01cc00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab7', 'hex'),\n        123,\n        'test_comment_4'\n    );\n\nINSERT INTO payments.internal_withdrawals (\n    failed,\n    since_lt,\n    sending_lt,\n    finish_lt,\n    created_at,\n    finished_at,\n    expired_at,\n    amount,\n    from_address,\n    memo\n) VALUES\n    (\n        --      finished withdrawal from TON deposit A after first payment\n        false,\n        1,\n        2,\n        2,\n        '2021-03-10 08:14:00 UTC',\n        '2021-03-10 08:15:00 UTC',\n        '2021-03-10 08:17:00 UTC',\n        100,\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7831'\n    ),\n    (\n        --      failed withdrawal from TON deposit A after second payment\n        true,\n        2,\n        2,\n        NULL,\n        '2021-03-10 08:16:00 UTC',\n        NULL,\n        '2021-03-10 08:19:00 UTC',\n        100,\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7832'\n    ),\n    (\n        --      not finished withdrawal from TON deposit B after payment\n        false,\n        1,\n        1,\n        NULL,\n        '2021-03-10 08:14:00 UTC',\n        NULL,\n        '2021-03-10 08:17:00 UTC',\n        100,\n        decode('01bb00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7833'\n    );\n\nINSERT INTO payments.ton_wallets (\n    subwallet_id,\n    created_at,\n    user_id,\n    type,\n    address\n) VALUES\n    (\n        --      TON hot wallet\n        1,\n        '2021-03-10 08:00:00 UTC',\n        '',\n        'ton_hot',\n        decode('00aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex')\n    ),\n    (\n        --      TON deposit A\n        2,\n        '2021-03-10 08:00:00 UTC',\n        'test_user',\n        'ton_deposit',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex')\n    ),\n    (\n        --      TON deposit B\n        3,\n        '2021-03-10 08:00:00 UTC',\n        'test_user',\n        'ton_deposit',\n        decode('01bb00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex')\n    ),\n    (\n        --      Jetton deposit C\n        4,\n        '2021-03-10 08:00:00 UTC',\n        'test_user',\n        'owner',\n        decode('01cc00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex')\n    );\n\nCOMMIT;"
  },
  {
    "path": "db/tests/set-expired/01_data.up.sql",
    "content": "BEGIN;\n\nINSERT INTO payments.external_withdrawals (\n    msg_uuid,\n    query_id,\n    created_at,\n    expired_at,\n    processed_at,\n    processed_lt,\n    confirmed,\n    failed,\n    address\n) VALUES\n    (\n        --      expired and not marked as expired 1st\n        'c2d29867-3d0b-d497-9191-18a9d8ee7831',\n        1,\n        '2000-01-01 08:00:00 UTC',\n        '2000-01-01 08:03:00 UTC',\n        NULL,\n        NULL,\n        false,\n        false,\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab1', 'hex')\n    ),\n    (\n        --      expired and not marked as expired 2nd\n        'c2d29867-3d0b-d497-9191-18a9d8ee7831',\n        2,\n        '2000-01-01 08:00:00 UTC',\n        '2000-01-01 08:04:00 UTC',\n        NULL,\n        NULL,\n        false,\n        false,\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex')\n    ),\n    (\n        --      expired and already marked as expired\n        'c2d29867-3d0b-d497-9191-18a9d8ee7831',\n        3,\n        '2000-01-01 08:00:00 UTC',\n        '2000-01-01 08:05:00 UTC',\n        NULL,\n        NULL,\n        false,\n        true,\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab3', 'hex')\n    ),\n    (\n        --      not expired\n        'c2d29867-3d0b-d497-9191-18a9d8ee7831',\n        4,\n        '2000-01-01 08:00:00 UTC',\n        '3000-01-01 08:00:00 UTC',\n        NULL,\n        NULL,\n        false,\n        false,\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab4', 'hex')\n    ),\n    (\n        --      not expired but failed\n        'c2d29867-3d0b-d497-9191-18a9d8ee7831',\n        5,\n        '2000-01-01 08:00:00 UTC',\n        '3000-01-01 08:00:00 UTC',\n        NULL,\n        NULL,\n        false,\n        true,\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab5', 'hex')\n    ),\n    (\n        --      not expired and already processed\n        'c2d29867-3d0b-d497-9191-18a9d8ee7831',\n        6,\n        '2000-01-01 08:00:00 UTC',\n        '3000-01-01 08:00:00 UTC',\n        '2000-01-01 08:10:00 UTC',\n        1,\n        false,\n        false,\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab6', 'hex')\n    ),\n    (\n        --      expired and already processed\n        'c2d29867-3d0b-d497-9191-18a9d8ee7831',\n        7,\n        '2000-01-01 08:00:00 UTC',\n        '2000-01-01 08:03:00 UTC',\n        '2000-01-01 08:01:00 UTC',\n        2,\n        false,\n        false,\n        decode('007424ee4767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab7', 'hex')\n    );\n\nINSERT INTO payments.internal_withdrawals (\n    failed,\n    since_lt,\n    sending_lt,\n    finish_lt,\n    created_at,\n    finished_at,\n    expired_at,\n    amount,\n    from_address,\n    memo\n) VALUES\n    (\n        --      expired and not marked as expired\n        false,\n        1,\n        NULL,\n        NULL,\n        '2020-03-10 08:00:00 UTC',\n        NULL,\n        '2020-03-10 08:03:00 UTC',\n        100,\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7831'\n    ),\n    (\n        --      expired and already marked as expired\n        true,\n        2,\n        NULL,\n        NULL,\n        '2020-03-10 08:00:00 UTC',\n        NULL,\n        '2020-03-10 08:03:00 UTC',\n        100,\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7832'\n    ),\n    (\n        --      not expired\n        false,\n        3,\n        4,\n        NULL,\n        '2020-03-10 08:00:00 UTC',\n        NULL,\n        '3020-03-10 08:00:00 UTC',\n        100,\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7833'\n    ),\n    (\n        --      not expired but failed\n        true,\n        4,\n        5,\n        NULL,\n        '2020-03-10 08:00:00 UTC',\n        NULL,\n        '3020-03-10 08:00:00 UTC',\n        100,\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7834'\n    ),\n    (\n        --      not expired but already processed\n        false,\n        5,\n        6,\n        6,\n        '2020-03-10 08:00:00 UTC',\n        '2020-03-10 08:01:00 UTC',\n        '3020-03-10 08:00:00 UTC',\n        100,\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7835'\n    ),\n    (\n        --      expired and already processed\n        false,\n        6,\n        7,\n        7,\n        '2020-03-10 08:00:00 UTC',\n        '2020-03-10 08:01:00 UTC',\n        '2020-03-10 08:03:00 UTC',\n        100,\n        decode('01bb00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex'),\n        'c2d29867-3d0b-d497-9191-18a9d8ee7836'\n    );\n\nINSERT INTO payments.withdrawal_requests (\n    query_id,\n    bounceable,\n    processing,\n    processed,\n    is_internal,\n    amount,\n    user_id,\n    user_query_id,\n    currency,\n    comment,\n    dest_address\n) VALUES\n    (\n        --      request with query_id 1 for external_withdrawals\n        1, false, true, false, false,\n        100, 'test_user', '1', 'TON', 'test_comment',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab1', 'hex')\n    ),\n    (\n        --      request with query_id 2 for external_withdrawals\n        2, false, true, false, false,\n        100, 'test_user', '2', 'TON', 'test_comment',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab2', 'hex')\n    ),\n    (\n        --      request with query_id 3 for external_withdrawals\n        3, false, false, false, false,\n        100, 'test_user', '3', 'TON', 'test_comment',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab3', 'hex')\n    ),\n    (\n        --      request with query_id 4 for external_withdrawals\n        4, false, true, false, false,\n        100, 'test_user', '4', 'TON', 'test_comment',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab4', 'hex')\n    ),\n    (\n        --      request with query_id 5 for external_withdrawals\n        5, false, false, false, false,\n        100, 'test_user', '5', 'TON', 'test_comment',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab5', 'hex')\n    ),\n    (\n        --      request with query_id 6 for external_withdrawals\n        6, false, true, true, false,\n        100, 'test_user', '6', 'TON', 'test_comment',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab6', 'hex')\n    ),\n    (\n        --      request with query_id 7 for external_withdrawals\n        7, false, true, true, false,\n        100, 'test_user', '7', 'TON', 'test_comment',\n        decode('01aa00004767fbcf859609200910269446980f4d27bd8f4e3faa6e4d74792ab7', 'hex')\n    );\n\nCOMMIT;"
  },
  {
    "path": "deploy/db/01_init.down.sql",
    "content": "BEGIN;\n\nDROP TABLE IF EXISTS payments.ton_wallets;\nDROP TABLE IF EXISTS payments.jetton_wallets;\nDROP TABLE IF EXISTS payments.internal_incomes;\nDROP TABLE IF EXISTS payments.external_withdrawals;\nDROP TABLE IF EXISTS payments.withdrawal_requests;\nDROP TABLE IF EXISTS payments.external_incomes;\nDROP TABLE IF EXISTS payments.block_data;\nDROP TABLE IF EXISTS payments.internal_withdrawals;\nDROP TABLE IF EXISTS payments.service_withdrawal_requests;\n\nDROP SCHEMA IF EXISTS payments;\n\nCOMMIT;"
  },
  {
    "path": "deploy/db/01_init.up.sql",
    "content": "BEGIN;\n\nCREATE SCHEMA IF NOT EXISTS payments;\n\nCREATE TABLE IF NOT EXISTS payments.ton_wallets\n(\n    subwallet_id     bigint not null unique, -- store uint32\n    created_at       timestamptz not null default now(),\n    user_id          text not null,\n    type             text not null,\n    address          bytea not null\n);\n\nCREATE INDEX IF NOT EXISTS ton_wallets_address_index\n    ON payments.ton_wallets (address);\n\nCREATE INDEX IF NOT EXISTS ton_wallets_type_index\n    ON payments.ton_wallets (type);\n\nCREATE INDEX IF NOT EXISTS ton_wallets_user_id_index\n    ON payments.ton_wallets (user_id);\n\nCREATE TABLE IF NOT EXISTS payments.jetton_wallets\n(\n    subwallet_id     bigint not null, -- store uint32\n    created_at       timestamptz not null default now(),\n    user_id          text not null,\n    currency         text not null,\n    type             text not null,\n    address          bytea not null unique\n);\n\nCREATE INDEX IF NOT EXISTS jetton_wallets_subwallet_id_index\n    ON payments.jetton_wallets (subwallet_id);\n\nCREATE TABLE IF NOT EXISTS payments.internal_incomes\n(\n    lt                    bigint not null,\n    utime                 timestamptz not null,\n    deposit_address       bytea not null,\n    amount                numeric not null,\n    memo                  uuid not null,\n    unique (memo, lt)\n);\n\nCREATE INDEX IF NOT EXISTS internal_incomes_deposit_address_index\n    ON payments.internal_incomes (deposit_address);\n\nCREATE TABLE IF NOT EXISTS payments.external_withdrawals\n(\n    msg_uuid         uuid not null,\n    query_id         bigint not null,\n    created_at       timestamptz not null default now(),\n    expired_at       timestamptz,\n    processed_at     timestamptz,\n    processed_lt     bigint,\n    confirmed        bool not null default false,\n    failed           bool not null default false,\n    address          bytea not null,\n    tx_hash          bytea,\n    unique (msg_uuid, address)\n);\n\nCREATE INDEX IF NOT EXISTS external_withdrawals_expired_at_index\n    ON payments.external_withdrawals (expired_at);\n\nCREATE INDEX IF NOT EXISTS external_withdrawals_msg_uuid_index\n    ON payments.external_withdrawals (msg_uuid);\n\nCREATE INDEX IF NOT EXISTS external_withdrawals_address_index\n    ON payments.external_withdrawals (address);\n\nCREATE INDEX IF NOT EXISTS external_withdrawals_query_id_index\n    ON payments.external_withdrawals (query_id);\n\nCREATE TABLE IF NOT EXISTS payments.withdrawal_requests\n(\n    query_id         bigserial\n                     constraint withdrawal_requests_pk\n                     primary key,\n    bounceable       bool  not null,\n    processing       bool not null default false,\n    processed        bool not null default false,\n    is_internal      bool default false,\n    amount           numeric not null,\n    user_id          text not null,\n    user_query_id    text not null,\n    currency         text not null,\n    dest_address     bytea not null,\n    comment          text,\n    binary_comment   text,\n    unique (user_id, user_query_id, is_internal)\n);\n\nCREATE INDEX IF NOT EXISTS withdrawal_requests_user_id_index\n    ON payments.withdrawal_requests (user_id);\n\nCREATE INDEX IF NOT EXISTS withdrawal_requests_user_query_id_index\n    ON payments.withdrawal_requests (user_query_id);\n\nCREATE INDEX IF NOT EXISTS withdrawal_requests_dest_address_index\n    ON payments.withdrawal_requests (dest_address);\n\nCREATE TABLE IF NOT EXISTS payments.external_incomes\n(\n    lt               bigint not null,\n    utime            timestamptz not null,\n    payer_workchain  integer,\n    deposit_address  bytea not null,\n    payer_address    bytea,\n    amount           numeric not null,\n    comment          text not null,\n    tx_hash          bytea\n);\n\nCREATE INDEX IF NOT EXISTS external_incomes_deposit_address_index\n    ON payments.external_incomes (deposit_address);\n\nCREATE TABLE IF NOT EXISTS payments.block_data\n(\n        saved_at                timestamptz not null default now(),\n        shard                   bigint not null,\n        seqno                   bigint not null,\n        gen_utime               timestamptz not null,\n        root_hash               bytea not null,\n        file_hash               bytea not null,\n        unique (shard, seqno)\n);\n\nCREATE INDEX IF NOT EXISTS block_data_seqno_index\n    ON payments.block_data (seqno);\n\nCREATE TABLE IF NOT EXISTS payments.internal_withdrawals\n(\n    failed           bool not null default false,\n    since_lt         bigint not null,    -- amount for this LT\n    sending_lt       bigint,             -- on wallet side\n    finish_lt        bigint,             -- on deposit side\n    finished_at      timestamptz,        -- on deposit side\n    created_at       timestamptz not null default now(),\n    expired_at       timestamptz,\n    amount           numeric not null default 0,\n    from_address     bytea not null,\n    memo             uuid not null unique,\n    unique (from_address, since_lt, memo)\n);\n\nCREATE INDEX IF NOT EXISTS internal_withdrawals_from_address_index\n    ON payments.internal_withdrawals (from_address);\n\nCREATE INDEX IF NOT EXISTS internal_withdrawals_since_lt_index\n    ON payments.internal_withdrawals (since_lt);\n\nCREATE INDEX IF NOT EXISTS internal_withdrawals_expired_at_index\n    ON payments.internal_withdrawals (expired_at);\n\nCREATE TABLE IF NOT EXISTS payments.service_withdrawal_requests\n(\n    memo uuid not null default gen_random_uuid() unique,\n    created_at       timestamptz not null default now(),\n    expired_at       timestamptz,\n    filled           bool not null default false,\n    processed        bool not null default false,\n    ton_amount       numeric not null default 0,\n    jetton_amount    numeric not null default 0,\n    from_address     bytea not null,\n    jetton_master    bytea\n);\n\nCOMMIT;"
  },
  {
    "path": "deploy/db/02_create_readonly_user.sh",
    "content": "#!/bin/bash\n\nif [ -z \"$POSTGRES_READONLY_PASSWORD\" ]; then\n  echo \"Environment variable POSTGRES_READONLY_PASSWORD is not set. Exiting.\"\n  exit 1\nfi\n\npsql -v ON_ERROR_STOP=1 --username \"$POSTGRES_USER\" --dbname \"$POSTGRES_DB\" <<-EOSQL\n  CREATE USER pp_readonly WITH PASSWORD '$POSTGRES_READONLY_PASSWORD';\n  GRANT CONNECT ON DATABASE $POSTGRES_DB TO pp_readonly;\n  GRANT USAGE ON SCHEMA payments TO pp_readonly;\n  GRANT SELECT ON ALL TABLES IN SCHEMA payments TO pp_readonly;\n  ALTER DEFAULT PRIVILEGES IN SCHEMA payments GRANT SELECT ON TABLES TO pp_readonly;\nEOSQL\n"
  },
  {
    "path": "deploy/grafana/main/dashboards/Payments.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_POSTGRES\",\n      \"label\": \"Postgres\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"postgres\",\n      \"pluginName\": \"PostgreSQL\"\n    }\n  ],\n  \"__elements\": [],\n  \"__requires\": [\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"8.3.6\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"postgres\",\n      \"name\": \"PostgreSQL\",\n      \"version\": \"1.0.0\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"table\",\n      \"name\": \"Table\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"target\": {\n          \"limit\": 100,\n          \"matchAny\": false,\n          \"tags\": [],\n          \"type\": \"dashboard\"\n        },\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 1,\n  \"id\": null,\n  \"links\": [],\n  \"liveNow\": true,\n  \"panels\": [\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"saved_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 184\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"seqno\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 210\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 2,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT saved_at, seqno, gen_utime\\nFROM payments.block_data\\nORDER BY seqno desc\\nLIMIT 4\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"paymnets.block_data\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Block data\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"query_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 71\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"processing\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 89\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"processed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 84\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 104\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 78\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"comment\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 358\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 0\n      },\n      \"id\": 4,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  query_id, processing, processed, amount, currency,comment\\nFROM payments.withdrawal_requests\\nORDER BY query_id DESC\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"payments.withdrawal_requests\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Withdrawal requests\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"utime\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 176\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 103\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"comment\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 134\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"subwallet_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 112\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 75\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 0\n      },\n      \"id\": 10,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  utime, amount, coalesce(tw.subwallet_id, jw.subwallet_id) as subwallet_id, coalesce(currency, 'TON') as currency, comment\\nFROM\\n  payments.external_incomes ei\\nLEFT JOIN payments.ton_wallets tw ON ei.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ei.deposit_address = jw.address\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"payments.external_incomes\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"External imcomes\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"subwallet_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 105\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"user_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 100\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"ton_type\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 214\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"jetton_type\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 104\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 80\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 12,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 6\n      },\n      \"id\": 30,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  tw.subwallet_id, tw.user_id, tw.type as ton_type,jw.type as jetton_type, coalesce(currency, 'TON') as Currency\\nFROM payments.ton_wallets tw\\nLEFT JOIN payments.jetton_wallets jw ON tw.subwallet_id = jw.subwallet_id\\nORDER BY 1\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"payments.ton_wallets\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Subwallets\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"query_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 70\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"confirmed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 87\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 151\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 73\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"failed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 56\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"created_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 148\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 6\n      },\n      \"id\": 12,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  ew.query_id,wr.amount,wr.currency,confirmed,failed,processed_at \\nFROM\\n  payments.external_withdrawals ew\\nLEFT JOIN payments.withdrawal_requests wr ON ew.query_id = wr.query_id\\nWHERE wr.is_internal=false\\n\\n  \\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"External withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"finished_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 149\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 92\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"from\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 89\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"created_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 150\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"memo\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 123\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 76\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 6\n      },\n      \"id\": 14,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  memo,amount,coalesce(jw.currency, 'TON') as currency,coalesce(tw.subwallet_id, jw.subwallet_id) as from, finished_at, failed\\nFROM\\n  payments.internal_withdrawals iw\\nLEFT JOIN payments.ton_wallets tw ON iw.from_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON iw.from_address = jw.address  \\n\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Internal withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"query_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 73\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 78\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"confirmed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 86\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"failed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 60\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 12\n      },\n      \"id\": 18,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  ew.query_id,wr.amount,wr.currency,confirmed,failed,processed_at \\nFROM\\n  payments.external_withdrawals ew\\nLEFT JOIN payments.withdrawal_requests wr ON ew.query_id = wr.query_id\\nWHERE wr.is_internal=true\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Cold wallet withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 139\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"memo\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 287\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 76\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"from\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 92\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 12\n      },\n      \"id\": 16,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  memo,amount,coalesce(jw.currency, 'TON') as currency,coalesce(tw.subwallet_id, jw.subwallet_id) as from\\nFROM\\n  payments.internal_incomes ii\\nLEFT JOIN payments.ton_wallets tw ON ii.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ii.deposit_address = jw.address  \\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Internal incomes\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 18\n      },\n      \"id\": 20,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  coalesce(tw.subwallet_id, jw.subwallet_id) as subwallet_id, SUM(amount) as total_amount, coalesce(jw.currency, 'TON') as currency\\nFROM\\n  payments.internal_incomes ii\\nLEFT JOIN payments.ton_wallets tw ON ii.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ii.deposit_address = jw.address\\nGROUP BY ii.deposit_address, tw.subwallet_id, jw.subwallet_id, jw.currency\\n\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Hot side deposit balances\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"processed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 87\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"ton_amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 103\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"jetton_amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 111\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 18\n      },\n      \"id\": 29,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT memo, processed, from_address, ton_amount, jetton_amount\\nFROM payments.service_withdrawal_requests\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"service_withdrawal_requests\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Service withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 18\n      },\n      \"id\": 21,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  coalesce(tw.subwallet_id, jw.subwallet_id) as subwallet_id, SUM(amount) as total_amount, coalesce(jw.currency, 'TON') as currency\\nFROM\\n  payments.external_incomes ei\\nLEFT JOIN payments.ton_wallets tw ON ei.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ei.deposit_address = jw.address\\nGROUP BY ei.deposit_address, tw.subwallet_id, jw.subwallet_id, jw.currency\\n\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Deposit side balances\",\n      \"type\": \"table\"\n    }\n  ],\n  \"refresh\": \"5s\",\n  \"schemaVersion\": 34,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": []\n  },\n  \"time\": {\n    \"from\": \"now-5m\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {\n    \"hidden\": false,\n    \"refresh_intervals\": [\n      \"5s\",\n      \"10s\",\n      \"30s\",\n      \"1m\",\n      \"5m\",\n      \"15m\",\n      \"30m\",\n      \"1h\",\n      \"2h\",\n      \"1d\"\n    ],\n    \"time_options\": [\n      \"5m\",\n      \"15m\",\n      \"1h\",\n      \"6h\",\n      \"12h\",\n      \"24h\",\n      \"2d\",\n      \"7d\",\n      \"30d\"\n    ],\n    \"type\": \"timepicker\"\n  },\n  \"timezone\": \"browser\",\n  \"title\": \"Payments\",\n  \"uid\": \"_QoyuqtVz\",\n  \"version\": 7,\n  \"weekStart\": \"\"\n}"
  },
  {
    "path": "deploy/grafana/main/provisioning/dashboards/payments.yml",
    "content": "apiVersion: 1\n\nproviders:\n  - name: dashboards\n    type: file\n    updateIntervalSeconds: 30\n    options:\n      path: /etc/dashboards\n      foldersFromFilesStructure: true\n"
  },
  {
    "path": "deploy/grafana/main/provisioning/datasources/data_sources.yml",
    "content": "apiVersion: 1\n\ndatasources:\n  - name: Prometheus\n    uid: DS_PROMETHEUS\n    type: prometheus\n    access: proxy\n    url: http://payment_prometheus:9090\n    isDefault: false\n\n  - name: Postgres\n    uid: DS_POSTGRES\n    type: postgres\n    url: payment_processor_db:5432\n    database: $POSTGRES_DB\n    user: pp_readonly\n    secureJsonData:\n      password: $POSTGRES_READONLY_PASSWORD\n    isDefault: true\n    jsonData:\n      sslmode: disable # disable/require/verify-ca/verify-full\n      maxOpenConns: 0 # Grafana v5.4+\n      maxIdleConns: 2 # Grafana v5.4+\n      connMaxLifetime: 14400 # Grafana v5.4+\n      postgresVersion: 903 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10\n      timescaledb: false\n"
  },
  {
    "path": "deploy/grafana/test/dashboards/Processor A.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_POSTGRES_A\",\n      \"label\": \"Postgres A\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"postgres\",\n      \"pluginName\": \"PostgreSQL\"\n    }\n  ],\n  \"__elements\": [],\n  \"__requires\": [\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"8.3.6\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"postgres\",\n      \"name\": \"PostgreSQL\",\n      \"version\": \"1.0.0\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"table\",\n      \"name\": \"Table\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"target\": {\n          \"limit\": 100,\n          \"matchAny\": false,\n          \"tags\": [],\n          \"type\": \"dashboard\"\n        },\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 1,\n  \"id\": null,\n  \"links\": [],\n  \"liveNow\": true,\n  \"panels\": [\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_A\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"saved_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 184\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"seqno\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 210\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 2,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_A\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT saved_at, seqno, gen_utime\\nFROM payments.block_data\\nORDER BY seqno desc\\nLIMIT 4\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"paymnets.block_data\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Block data\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_A\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"query_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 71\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"processing\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 89\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"processed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 84\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 104\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 78\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"comment\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 358\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 0\n      },\n      \"id\": 4,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_A\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  query_id, processing, processed, amount, currency,comment\\nFROM payments.withdrawal_requests\\nORDER BY query_id DESC\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"payments.withdrawal_requests\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Withdrawal requests\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_A\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"utime\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 176\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 103\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"comment\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 134\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"subwallet_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 112\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 75\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 0\n      },\n      \"id\": 10,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_A\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  utime, amount, coalesce(tw.subwallet_id, jw.subwallet_id) as subwallet_id, coalesce(currency, 'TON') as currency, comment\\nFROM\\n  payments.external_incomes ei\\nLEFT JOIN payments.ton_wallets tw ON ei.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ei.deposit_address = jw.address\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"payments.external_incomes\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"External imcomes\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_A\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"subwallet_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 105\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"user_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 100\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"ton_type\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 214\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"jetton_type\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 104\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 80\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 12,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 6\n      },\n      \"id\": 30,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_A\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  tw.subwallet_id, tw.user_id, tw.type as ton_type,jw.type as jetton_type, coalesce(currency, 'TON') as Currency\\nFROM payments.ton_wallets tw\\nLEFT JOIN payments.jetton_wallets jw ON tw.subwallet_id = jw.subwallet_id\\nORDER BY 1\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"payments.ton_wallets\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Subwallets\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_A\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"query_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 70\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"confirmed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 87\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 151\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 73\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"failed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 56\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"created_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 148\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 6\n      },\n      \"id\": 12,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_A\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  ew.query_id,wr.amount,wr.currency,confirmed,failed,processed_at \\nFROM\\n  payments.external_withdrawals ew\\nLEFT JOIN payments.withdrawal_requests wr ON ew.query_id = wr.query_id\\nWHERE wr.is_internal=false\\n\\n  \\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"External withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_A\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"finished_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 149\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 92\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"from\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 89\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"created_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 150\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"memo\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 123\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 76\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 6\n      },\n      \"id\": 14,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_A\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  memo,amount,coalesce(jw.currency, 'TON') as currency,coalesce(tw.subwallet_id, jw.subwallet_id) as from, finished_at, failed\\nFROM\\n  payments.internal_withdrawals iw\\nLEFT JOIN payments.ton_wallets tw ON iw.from_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON iw.from_address = jw.address  \\n\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Internal withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_A\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"query_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 73\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 78\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"confirmed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 86\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"failed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 60\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 12\n      },\n      \"id\": 18,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_A\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  ew.query_id,wr.amount,wr.currency,confirmed,failed,processed_at \\nFROM\\n  payments.external_withdrawals ew\\nLEFT JOIN payments.withdrawal_requests wr ON ew.query_id = wr.query_id\\nWHERE wr.is_internal=true\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Cold wallet withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_A\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 139\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"memo\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 287\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 76\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"from\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 92\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 12\n      },\n      \"id\": 16,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_A\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  memo,amount,coalesce(jw.currency, 'TON') as currency,coalesce(tw.subwallet_id, jw.subwallet_id) as from\\nFROM\\n  payments.internal_incomes ii\\nLEFT JOIN payments.ton_wallets tw ON ii.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ii.deposit_address = jw.address  \\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Internal incomes\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_A\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 18\n      },\n      \"id\": 20,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_A\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  coalesce(tw.subwallet_id, jw.subwallet_id) as subwallet_id, SUM(amount) as total_amount, coalesce(jw.currency, 'TON') as currency\\nFROM\\n  payments.internal_incomes ii\\nLEFT JOIN payments.ton_wallets tw ON ii.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ii.deposit_address = jw.address\\nGROUP BY ii.deposit_address, tw.subwallet_id, jw.subwallet_id, jw.currency\\n\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Hot side deposit balances\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_A\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"processed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 87\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"ton_amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 103\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"jetton_amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 111\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 18\n      },\n      \"id\": 29,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_A\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT memo, processed, from_address, ton_amount, jetton_amount\\nFROM payments.service_withdrawal_requests\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"service_withdrawal_requests\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Service withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_A\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 18\n      },\n      \"id\": 21,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_A\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  coalesce(tw.subwallet_id, jw.subwallet_id) as subwallet_id, SUM(amount) as total_amount, coalesce(jw.currency, 'TON') as currency\\nFROM\\n  payments.external_incomes ei\\nLEFT JOIN payments.ton_wallets tw ON ei.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ei.deposit_address = jw.address\\nGROUP BY ei.deposit_address, tw.subwallet_id, jw.subwallet_id, jw.currency\\n\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Deposit side balances\",\n      \"type\": \"table\"\n    }\n  ],\n  \"refresh\": \"5s\",\n  \"schemaVersion\": 34,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": []\n  },\n  \"time\": {\n    \"from\": \"now-5m\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {\n    \"hidden\": false,\n    \"refresh_intervals\": [\n      \"5s\",\n      \"10s\",\n      \"30s\",\n      \"1m\",\n      \"5m\",\n      \"15m\",\n      \"30m\",\n      \"1h\",\n      \"2h\",\n      \"1d\"\n    ],\n    \"time_options\": [\n      \"5m\",\n      \"15m\",\n      \"1h\",\n      \"6h\",\n      \"12h\",\n      \"24h\",\n      \"2d\",\n      \"7d\",\n      \"30d\"\n    ],\n    \"type\": \"timepicker\"\n  },\n  \"timezone\": \"browser\",\n  \"title\": \"Processor A\",\n  \"uid\": \"_QoyuqtVz\",\n  \"version\": 7,\n  \"weekStart\": \"\"\n}"
  },
  {
    "path": "deploy/grafana/test/dashboards/Processor B.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_POSTGRES_B\",\n      \"label\": \"Postgres B\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"postgres\",\n      \"pluginName\": \"PostgreSQL\"\n    }\n  ],\n  \"__elements\": [],\n  \"__requires\": [\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"8.3.6\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"postgres\",\n      \"name\": \"PostgreSQL\",\n      \"version\": \"1.0.0\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"table\",\n      \"name\": \"Table\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"target\": {\n          \"limit\": 100,\n          \"matchAny\": false,\n          \"tags\": [],\n          \"type\": \"dashboard\"\n        },\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 1,\n  \"id\": null,\n  \"links\": [],\n  \"liveNow\": true,\n  \"panels\": [\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_B\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"saved_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 184\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"seqno\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 210\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 2,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_B\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT saved_at, seqno, gen_utime\\nFROM payments.block_data\\nORDER BY seqno desc\\nLIMIT 4\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"paymnets.block_data\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Block data\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_B\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"query_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 71\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"processing\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 89\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"processed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 84\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 104\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 78\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"comment\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 358\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 0\n      },\n      \"id\": 4,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_B\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  query_id, processing, processed, amount, currency,comment\\nFROM payments.withdrawal_requests\\nORDER BY query_id DESC\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"payments.withdrawal_requests\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Withdrawal requests\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_B\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"utime\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 176\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 103\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"comment\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 134\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"subwallet_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 112\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 75\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 0\n      },\n      \"id\": 10,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_B\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  utime, amount, coalesce(tw.subwallet_id, jw.subwallet_id) as subwallet_id, coalesce(currency, 'TON') as currency, comment\\nFROM\\n  payments.external_incomes ei\\nLEFT JOIN payments.ton_wallets tw ON ei.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ei.deposit_address = jw.address\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"payments.external_incomes\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"External imcomes\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_B\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"subwallet_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 105\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"user_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 100\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"ton_type\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 214\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"jetton_type\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 104\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 80\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 12,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 6\n      },\n      \"id\": 30,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_B\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  tw.subwallet_id, tw.user_id, tw.type as ton_type,jw.type as jetton_type, coalesce(currency, 'TON') as Currency\\nFROM payments.ton_wallets tw\\nLEFT JOIN payments.jetton_wallets jw ON tw.subwallet_id = jw.subwallet_id\\nORDER BY 1\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"payments.ton_wallets\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Subwallets\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_B\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"query_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 70\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"confirmed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 87\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 151\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 73\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"failed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 56\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"created_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 148\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 6\n      },\n      \"id\": 12,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_B\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  ew.query_id,wr.amount,wr.currency,confirmed,failed,processed_at \\nFROM\\n  payments.external_withdrawals ew\\nLEFT JOIN payments.withdrawal_requests wr ON ew.query_id = wr.query_id\\nWHERE wr.is_internal=false\\n\\n  \\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"External withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_B\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"finished_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 149\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 92\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"from\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 89\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"created_at\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 150\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"memo\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 123\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 76\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 6\n      },\n      \"id\": 14,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_B\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  memo,amount,coalesce(jw.currency, 'TON') as currency,coalesce(tw.subwallet_id, jw.subwallet_id) as from, finished_at, failed\\nFROM\\n  payments.internal_withdrawals iw\\nLEFT JOIN payments.ton_wallets tw ON iw.from_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON iw.from_address = jw.address  \\n\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Internal withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_B\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"query_id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 73\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 78\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"confirmed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 86\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"failed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 60\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 12\n      },\n      \"id\": 18,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_B\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  ew.query_id,wr.amount,wr.currency,confirmed,failed,processed_at \\nFROM\\n  payments.external_withdrawals ew\\nLEFT JOIN payments.withdrawal_requests wr ON ew.query_id = wr.query_id\\nWHERE wr.is_internal=true\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Cold wallet withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_B\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 139\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"memo\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 287\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"currency\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 76\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"from\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 92\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 12\n      },\n      \"id\": 16,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_B\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  memo,amount,coalesce(jw.currency, 'TON') as currency,coalesce(tw.subwallet_id, jw.subwallet_id) as from\\nFROM\\n  payments.internal_incomes ii\\nLEFT JOIN payments.ton_wallets tw ON ii.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ii.deposit_address = jw.address  \\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Internal incomes\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_B\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 18\n      },\n      \"id\": 20,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_B\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  coalesce(tw.subwallet_id, jw.subwallet_id) as subwallet_id, SUM(amount) as total_amount, coalesce(jw.currency, 'TON') as currency\\nFROM\\n  payments.internal_incomes ii\\nLEFT JOIN payments.ton_wallets tw ON ii.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ii.deposit_address = jw.address\\nGROUP BY ii.deposit_address, tw.subwallet_id, jw.subwallet_id, jw.currency\\n\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Hot side deposit balances\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_B\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"processed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 87\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"ton_amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 103\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"jetton_amount\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 111\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 18\n      },\n      \"id\": 29,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_B\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT memo, processed, from_address, ton_amount, jetton_amount\\nFROM payments.service_withdrawal_requests\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"table\": \"service_withdrawal_requests\",\n          \"timeColumn\": \"time\",\n          \"where\": []\n        }\n      ],\n      \"title\": \"Service withdrawals\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"postgres\",\n        \"uid\": \"DS_POSTGRES_B\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 18\n      },\n      \"id\": 21,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\n            \"sum\"\n          ],\n          \"show\": false\n        },\n        \"showHeader\": true\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"postgres\",\n            \"uid\": \"DS_POSTGRES_B\"\n          },\n          \"format\": \"table\",\n          \"group\": [],\n          \"hide\": false,\n          \"metricColumn\": \"none\",\n          \"rawQuery\": true,\n          \"rawSql\": \"SELECT\\n  coalesce(tw.subwallet_id, jw.subwallet_id) as subwallet_id, SUM(amount) as total_amount, coalesce(jw.currency, 'TON') as currency\\nFROM\\n  payments.external_incomes ei\\nLEFT JOIN payments.ton_wallets tw ON ei.deposit_address = tw.address \\nLEFT JOIN payments.jetton_wallets jw ON ei.deposit_address = jw.address\\nGROUP BY ei.deposit_address, tw.subwallet_id, jw.subwallet_id, jw.currency\\n\\n\",\n          \"refId\": \"A\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"column\"\n              }\n            ]\n          ],\n          \"timeColumn\": \"time\",\n          \"where\": [\n            {\n              \"name\": \"$__timeFilter\",\n              \"params\": [],\n              \"type\": \"macro\"\n            }\n          ]\n        }\n      ],\n      \"title\": \"Deposit side balances\",\n      \"type\": \"table\"\n    }\n  ],\n  \"refresh\": \"5s\",\n  \"schemaVersion\": 34,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": []\n  },\n  \"time\": {\n    \"from\": \"now-5m\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {\n    \"hidden\": false,\n    \"refresh_intervals\": [\n      \"5s\",\n      \"10s\",\n      \"30s\",\n      \"1m\",\n      \"5m\",\n      \"15m\",\n      \"30m\",\n      \"1h\",\n      \"2h\",\n      \"1d\"\n    ],\n    \"time_options\": [\n      \"5m\",\n      \"15m\",\n      \"1h\",\n      \"6h\",\n      \"12h\",\n      \"24h\",\n      \"2d\",\n      \"7d\",\n      \"30d\"\n    ],\n    \"type\": \"timepicker\"\n  },\n  \"timezone\": \"browser\",\n  \"title\": \"Processor B\",\n  \"uid\": \"DaYu9qtVk\",\n  \"version\": 2,\n  \"weekStart\": \"\"\n}"
  },
  {
    "path": "deploy/grafana/test/dashboards/Test util.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_PROMETHEUS\",\n      \"label\": \"Prometheus\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"prometheus\",\n      \"pluginName\": \"Prometheus\"\n    }\n  ],\n  \"__elements\": [],\n  \"__requires\": [\n    {\n      \"type\": \"panel\",\n      \"id\": \"bargauge\",\n      \"name\": \"Bar gauge\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"8.3.6\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"prometheus\",\n      \"name\": \"Prometheus\",\n      \"version\": \"1.0.0\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"stat\",\n      \"name\": \"Stat\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"target\": {\n          \"limit\": 100,\n          \"matchAny\": false,\n          \"tags\": [],\n          \"type\": \"dashboard\"\n        },\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 1,\n  \"id\": null,\n  \"links\": [],\n  \"liveNow\": true,\n  \"panels\": [\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"DS_PROMETHEUS\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"decimals\": 3,\n          \"mappings\": [],\n          \"max\": 50,\n          \"min\": 0,\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"red\",\n                \"value\": null\n              },\n              {\n                \"color\": \"yellow\",\n                \"value\": 20\n              },\n              {\n                \"color\": \"green\",\n                \"value\": 50\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 7,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 8,\n      \"options\": {\n        \"displayMode\": \"gradient\",\n        \"orientation\": \"vertical\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"showUnfilled\": true\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"DS_PROMETHEUS\"\n          },\n          \"exemplar\": false,\n          \"expr\": \"hot_wallet_a_balance{}/1000000000\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{currency}}: A\",\n          \"refId\": \"Payer\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"DS_PROMETHEUS\"\n          },\n          \"exemplar\": true,\n          \"expr\": \"hot_wallet_b_balance{}/1000000000\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{currency}}: B\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Hot wallets balance\",\n      \"transformations\": [],\n      \"type\": \"bargauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"DS_PROMETHEUS\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"decimals\": 3,\n          \"mappings\": [],\n          \"max\": 100,\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 5,\n        \"x\": 7,\n        \"y\": 0\n      },\n      \"id\": 28,\n      \"options\": {\n        \"displayMode\": \"gradient\",\n        \"orientation\": \"vertical\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"showUnfilled\": true\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"DS_PROMETHEUS\"\n          },\n          \"exemplar\": true,\n          \"expr\": \"total_balance{}/1000000000\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{currency}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Total balance\",\n      \"transformations\": [],\n      \"type\": \"bargauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"DS_PROMETHEUS\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"decimals\": 3,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"red\",\n                \"value\": null\n              },\n              {\n                \"color\": \"green\",\n                \"value\": -1\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 4,\n        \"x\": 12,\n        \"y\": 0\n      },\n      \"id\": 32,\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"horizontal\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"DS_PROMETHEUS\"\n          },\n          \"exemplar\": true,\n          \"expr\": \"total_processed_amount{}/1000000000\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{currency}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Processed amount\",\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"DS_PROMETHEUS\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"red\",\n                \"value\": null\n              },\n              {\n                \"color\": \"green\",\n                \"value\": -1\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 4,\n        \"x\": 16,\n        \"y\": 0\n      },\n      \"id\": 25,\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"horizontal\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"DS_PROMETHEUS\"\n          },\n          \"exemplar\": true,\n          \"expr\": \"total_losses{}/-1000000000\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{currency}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Losses\",\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"DS_PROMETHEUS\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"displayName\": \"TON\",\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"red\",\n                \"value\": null\n              },\n              {\n                \"color\": \"green\",\n                \"value\": -1\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 2,\n        \"x\": 20,\n        \"y\": 0\n      },\n      \"id\": 31,\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"vertical\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"DS_PROMETHEUS\"\n          },\n          \"exemplar\": false,\n          \"expr\": \"predicted_ton_loss{}/1000000000\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"\",\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Predicted loss\",\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"DS_PROMETHEUS\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"m\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 2,\n        \"x\": 22,\n        \"y\": 0\n      },\n      \"id\": 27,\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"none\",\n        \"justifyMode\": \"center\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"value\"\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"DS_PROMETHEUS\"\n          },\n          \"exemplar\": true,\n          \"expr\": \"(time() - process_start_time_seconds{})/60\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Uptime\",\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"DS_PROMETHEUS\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"decimals\": 3,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 7\n      },\n      \"id\": 23,\n      \"options\": {\n        \"displayMode\": \"gradient\",\n        \"orientation\": \"horizontal\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"showUnfilled\": true\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"DS_PROMETHEUS\"\n          },\n          \"exemplar\": true,\n          \"expr\": \"deposit_wallet_a_balance{}/1000000000\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{currency}}: {{address}} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Deposit A balances\",\n      \"type\": \"bargauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"DS_PROMETHEUS\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"decimals\": 3,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 16,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 7\n      },\n      \"id\": 30,\n      \"options\": {\n        \"displayMode\": \"gradient\",\n        \"orientation\": \"horizontal\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"showUnfilled\": true\n      },\n      \"pluginVersion\": \"8.3.6\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"DS_PROMETHEUS\"\n          },\n          \"exemplar\": true,\n          \"expr\": \"deposit_wallet_b_balance{}/1000000000\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{currency}}:  {{address}} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Deposit B balances\",\n      \"type\": \"bargauge\"\n    }\n  ],\n  \"refresh\": \"5s\",\n  \"schemaVersion\": 34,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": []\n  },\n  \"time\": {\n    \"from\": \"now-1h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {\n    \"hidden\": false,\n    \"refresh_intervals\": [\n      \"5s\",\n      \"10s\",\n      \"30s\",\n      \"1m\",\n      \"5m\",\n      \"15m\",\n      \"30m\",\n      \"1h\",\n      \"2h\",\n      \"1d\"\n    ],\n    \"time_options\": [\n      \"5m\",\n      \"15m\",\n      \"1h\",\n      \"6h\",\n      \"12h\",\n      \"24h\",\n      \"2d\",\n      \"7d\",\n      \"30d\"\n    ],\n    \"type\": \"timepicker\"\n  },\n  \"timezone\": \"browser\",\n  \"title\": \"Test util\",\n  \"uid\": \"o-Hru3t4k\",\n  \"version\": 4,\n  \"weekStart\": \"\"\n}"
  },
  {
    "path": "deploy/grafana/test/provisioning/dashboards/payments.yml",
    "content": "apiVersion: 1\n\nproviders:\n  - name: dashboards\n    type: file\n    updateIntervalSeconds: 30\n    options:\n      path: /etc/dashboards\n      foldersFromFilesStructure: true\n"
  },
  {
    "path": "deploy/grafana/test/provisioning/datasources/data_sources.yml",
    "content": "apiVersion: 1\n\ndatasources:\n  - name: Prometheus\n    uid: DS_PROMETHEUS\n    type: prometheus\n    access: proxy\n    url: http://payment_prometheus:9090\n    isDefault: false\n\n  - name: Postgres A\n    uid: DS_POSTGRES_A\n    type: postgres\n    url: payment_processor_db_a:5432\n    database: payment_processor\n    user: pp_user\n    secureJsonData:\n      password: $POSTGRES_PASSWORD\n    isDefault: false\n    jsonData:\n      sslmode: disable # disable/require/verify-ca/verify-full\n      maxOpenConns: 0 # Grafana v5.4+\n      maxIdleConns: 2 # Grafana v5.4+\n      connMaxLifetime: 14400 # Grafana v5.4+\n      postgresVersion: 903 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10\n      timescaledb: false\n\n  - name: Postgres B\n    uid: DS_POSTGRES_B\n    type: postgres\n    url: payment_processor_db_b:5432\n    database: payment_processor\n    user: pp_user\n    secureJsonData:\n      password: $POSTGRES_PASSWORD\n    isDefault: false\n    jsonData:\n      sslmode: disable # disable/require/verify-ca/verify-full\n      maxOpenConns: 0 # Grafana v5.4+\n      maxIdleConns: 2 # Grafana v5.4+\n      connMaxLifetime: 14400 # Grafana v5.4+\n      postgresVersion: 903 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10\n      timescaledb: false\n"
  },
  {
    "path": "deploy/manual_migrations/0.1.x-0.2.0.sql",
    "content": "BEGIN;\n\nALTER TABLE payments.external_incomes\nADD COLUMN IF NOT EXISTS payer_workchain integer;\n\nUPDATE payments.external_incomes SET payer_workchain = 0 WHERE payer_address IS NOT NULL AND payer_workchain IS NULL;  -- all existing addresses will be marked as 0 workchain\n\nCOMMIT;"
  },
  {
    "path": "deploy/manual_migrations/0.4.x-0.5.0.sql",
    "content": "BEGIN;\n\nALTER TABLE payments.external_withdrawals\n    ADD COLUMN IF NOT EXISTS tx_hash bytea;\n\nALTER TABLE payments.withdrawal_requests\n    ADD COLUMN IF NOT EXISTS binary_comment text default '';\n\nALTER TABLE payments.external_incomes\n    ADD COLUMN IF NOT EXISTS tx_hash bytea;\n\nCOMMIT;"
  },
  {
    "path": "deploy/prometheus/main/prometheus.yml",
    "content": "scrape_configs:\n\n  - job_name: audit-metrics\n    scrape_interval: 5s\n    static_configs:\n      - targets: ['payment-processor:8081']\n"
  },
  {
    "path": "deploy/prometheus/test/prometheus.yml",
    "content": "scrape_configs:\n\n  - job_name: test-utils\n    scrape_interval: 5s\n    static_configs:\n      - targets: ['payment_test:9101']\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\n\nservices:\n\n  payment-postgres:\n    image: postgres:14\n    container_name: payment_processor_db\n    volumes:\n    - ./deploy/db/01_init.up.sql:/docker-entrypoint-initdb.d/01_init.up.sql\n    - ./deploy/db/02_create_readonly_user.sh:/docker-entrypoint-initdb.d/02_create_readonly_user.sh\n    - bicycle-postgres:/var/lib/postgresql/data\n    restart: always\n    environment:\n      POSTGRES_DB: ${POSTGRES_DB}\n      POSTGRES_USER: ${POSTGRES_USER}\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n      POSTGRES_READONLY_PASSWORD: ${POSTGRES_READONLY_PASSWORD} # For grafana\n    networks:\n      - p-network\n\n  payment-processor:\n    image: payment-processor\n    container_name: payment_processor\n    ports:\n      - \"127.0.0.1:8081:${API_PORT}\"\n    restart: unless-stopped\n    environment:\n      DB_URI: ${DB_URI} # example: \"postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@payment_processor_db:5432/${POSTGRES_DB}\"\n      API_PORT: ${API_PORT}\n      API_TOKEN: ${API_TOKEN}\n      COLD_WALLET: ${COLD_WALLET}\n      JETTONS: ${JETTONS}\n      LITESERVER: ${LITESERVER}\n      LITESERVER_KEY: ${LITESERVER_KEY}\n      SEED: ${SEED}\n      TON_CUTOFFS: ${TON_CUTOFFS}\n      IS_TESTNET: ${IS_TESTNET}\n    networks:\n      - p-network\n\n  payment-grafana:\n    image: grafana/grafana:latest\n    container_name: payment_grafana\n    restart: always\n    ports:\n      - '127.0.0.1:3001:3000'\n    volumes:\n      - ./deploy/grafana/main/provisioning/datasources:/etc/grafana/provisioning/datasources\n      - ./deploy/grafana/main/provisioning/dashboards:/etc/grafana/provisioning/dashboards\n      - ./deploy/grafana/main/dashboards:/etc/dashboards\n    environment:\n      GF_SECURITY_ADMIN_USER: admin # TODO: change\n      GF_SECURITY_ADMIN_PASSWORD: admin # TODO: change\n      POSTGRES_DB: ${POSTGRES_DB}\n      POSTGRES_READONLY_PASSWORD: ${POSTGRES_READONLY_PASSWORD}\n    networks:\n      - p-network\n\n  payment-rabbitmq:\n    image: library/rabbitmq:3-management\n    container_name: payment_rabbitmq\n    restart: always\n    ports:\n      - '127.0.0.1:5672:5672'\n    networks:\n      - p-network\n\n  payment-prometheus:\n    image: prom/prometheus:latest\n    container_name: payment_prometheus\n    restart: always\n    volumes:\n      - ./deploy/prometheus/main:/etc/prometheus\n    user: \"$UID:$GID\"\n    command:\n      - '--config.file=/etc/prometheus/prometheus.yml'\n    networks:\n      - p-network\n\nnetworks:\n  p-network:\n    driver: bridge\n\nvolumes:\n  bicycle-postgres:\n    external: false"
  },
  {
    "path": "docs/api.apib",
    "content": "FORMAT: 1A\n\n# Payment processor API\nThis API describes endpoints of payment processor.\n\n## New address [POST /v1/address/new]\n\nGenerates new deposit address\n\n+ Request (application/json)\n\n    + Headers\n\n            Authorization: Bearer\n    + Body\n\n            {\"user_id\": \"123\", \"currency\": \"TON\"}\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n              \"address\": \"0QB7BSerVyP9xAKnxp3QpqR8JO2HKwZhl10zsfwg7aJ281ZR\"\n            }\n\n+ Response 400 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 401          \n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }            \n\n\n## Get all addresses [GET /v1/address/all{?user_id}]\n\nGet all created addresses by `user_id`\n\n+ Parameters\n\n    + user_id (string) - an unique identifier of the user\n\n+ Request\n\n    + Headers\n\n            Authorization: Bearer\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n              \"addresses\": [\n                {\n                  \"address\": \"0QB7BSerVyP9xAKnxp3QpqR8JO2HKwZhl10zsfwg7aJ281ZR\",\n                  \"currency\": \"TON\"\n                },\n                {\n                  \"address\": \"0QCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseHynZ_YiH9Y1oSe\",\n                  \"currency\": \"TGR\"\n                }\n              ]\n            }\n\n+ Response 400 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 401\n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            } \n\n## Get income [GET /v1/income{?user_id}]\n\nGet income for deposits by `user_id`. The total amount of funds that came to the deposit for the entire time.\nFunds sent from the deposit to the hot wallet or all funds received to the deposit are taken into account,\ndepending on the service settings.\nCounting side field options: \"hot_wallet\", \"deposit\".\n\n+ Parameters\n\n    + user_id (string) - an unique identifier of the user\n\n+ Request\n\n    + Headers\n\n            Authorization: Bearer\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n              \"counting_side\": \"deposit\",\n              \"total_income\": [\n                {\n                  \"deposit_address\": \"0QB7BSerVyP9xAKnxp3QpqR8JO2HKwZhl10zsfwg7aJ281ZR\",\n                  \"amount\": \"1000000\",\n                  \"currency\": \"TON\"\n                },\n                {\n                  \"deposit_address\": \"0QCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseHynZ_YiH9Y1oSe\",\n                  \"amount\": \"1023000\",\n                  \"currency\": \"TGR\"\n                }\n              ]\n            }\n\n+ Response 400 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 401\n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            } \n\n## Get history [GET /v1/deposit/history{?user_id,currency,limit,offset,sort_order}]\n\nGet history for deposits by `user_id` and `currency`. Returns the history of all deposits replenishments with the\nsender's address as `source_address` (if it could be determined).\n\n+ Parameters\n\n    + user_id (string) - an unique identifier of the user\n    + currency (string) - the text identifier of the currency specified in the processor configuration. `TON` for TON coin.\n    + limit (number) - the maximum value of returned records\n    + offset (number) - offset for returned records\n    + sort_order (string) - asc or desc. desc by default if the parameter is not specified.\n\n+ Request\n\n    + Headers\n\n            Authorization: Bearer\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n              \"incomes\": [\n                {\n                  \"deposit_address\": \"0QB7BSerVyP9xAKnxp3QpqR8JO2HKwZhl10zsfwg7aJ281ZR\",\n                  \"time\": 1680604643,\n                  \"source_address\": \"0QAUuul9LdYcyJuBHernHo3JkbWTduH_FuEb2H8jCDdGesOP\",\n                  \"amount\": \"1000000\",\n                  \"comment\": \"hello\",\n                  \"tx_hash\": \"9d0fb69b1ca9371bc9e5260a248cda12a1c42916f9051fe9fc21b4abdd41d744\"\n                },\n                {\n                  \"deposit_address\": \"0QCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseHynZ_YiH9Y1oSe\",\n                  \"time\": 1680604648,\n                  \"source_address\": \"0QAUuul9LdYcyJuBHernHo3JkbWTduH_FuEb2H8jCDdGesOP\",\n                  \"amount\": \"1000000\",\n                  \"comment\": \"hello\",\n                  \"tx_hash\": \"7d0fb69b1ca9371bc9e5260a248cda12a1c42916f9051fe9fc21b4abdd41d744\"\n                }\n              ]\n            }\n\n+ Response 400 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 401\n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n## Send withdrawal [POST /v1/withdrawal/send]\n\nSend withdrawal request. **Amount must be in base units without decimal point (NanoTONs for TONs)**\nInstead of a `comment`, you can specify a `binary_comment` in hex notation format.\n`binary_comment` supports binary string hex representation form with flip bit (example: `9fe7_`)\n\n+ Request (application/json)\n\n    + Headers\n\n            Authorization: Bearer\n    + Body\n\n            {\n              \"user_id\": \"123\",\n              \"query_id\": \"321\",\n              \"currency\": \"TON\",\n              \"amount\":  \"100\",\n              \"destination\": \"0QCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseHynZ_YiH9Y1oSe\",\n              \"comment\":  \"hello\"\n            }\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n              \"ID\": 1\n            }\n\n+ Response 400 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 401\n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            } \n\n## Send service withdrawal [POST /v1/withdrawal/service/ton]\n\nSend service withdrawal request. Withdraw all TONs from `from` address to hot wallet.\nReturns `memo` as comment for transfer message.\n\n+ Request (application/json)\n\n    + Headers\n\n            Authorization: Bearer\n    + Body\n\n            {\n              \"from\": \"0QAUuul9LdYcyJuBHernHo3JkbWTduH_FuEb2H8jCDdGesOP\",\n            }\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n              \"memo\": \"123e4567-e89b-12d3-a456-426655440000\"\n            }\n\n+ Response 400 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 401\n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            } \n\n## Send service withdrawal [POST /v1/withdrawal/service/jetton]\n\nSend service withdrawal request. Withdraw all Jettons from Jetton wallet. Address calculated through owner and Jetton master.\nReturns `memo` as comment for transfer message.\n\n+ Request (application/json)\n\n    + Headers\n\n            Authorization: Bearer\n    + Body\n\n            {\n              \"owner\": \"0QAUuul9LdYcyJuBHernHo3JkbWTduH_FuEb2H8jCDdGesOP\",\n              \"jetton_master\": \"kQCKt2WPGX-fh0cIAz38Ljd_OKQjoZE_cqk7QrYGsNP6wfP0\",\n            }\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n              \"memo\": \"123e4567-e89b-12d3-a456-426655440000\"\n            }\n\n+ Response 400 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 401\n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            } \n\n## Get withdrawal status [GET /v1/withdrawal/status{?id}]\n\nGet withdrawal status. Returns `pending`, `processing`, `processed`/'failed', transaction hash for processed withdrawal and request meta (user_id and query_id).\n\n+ Parameters\n\n    + id (number) - An unique identifier of the withdrawal.\n\n+ Request (application/json)\n\n    + Headers\n\n            Authorization: Bearer\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n              \"user_id\": \"123\",\n              \"query_id\": \"321\",\n              \"status\": \"processed\",\n              \"tx_hash\": \"9d0fb69b1ca9371bc9e5260a248cda12a1c42916f9051fe9fc21b4abdd41d744\"\n            }\n\n+ Response 400 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 401            \n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            } \n\n## Get balance [GET /v1/balance{?currency,address}]\n\nGet balance for account by `address` and `currency`. Returns the balance of hot wallet if `address` not specified.\nFor hot wallet also returns `total_processing_amount` and `total_pending_amount` for withdrawals in queue.\nFor TON also returns account status: \"active\", \"uninit\", \"frozen\", \"non_exist\".\n\n+ Parameters\n\n    + currency (string) - the text identifier of the currency specified in the processor configuration. `TON` for TON coin.\n    + address (string) - address in URL-safe, user-friendly Base64 form. Hot wallet address by default if the parameter is not specified.\n\n+ Request\n\n    + Headers\n\n            Authorization: Bearer\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n                \"balance\": \"1000000\",\n                \"status\": \"active\",\n                \"total_processing_amount\": \"1000\",\n                \"total_pending_amount\": \"1000\"\n            }\n\n+ Response 400 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 401\n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n## Get sync flag [GET /v1/system/sync]\n\nGet blockchain sync flag. Returns `true` if the service has up-to-date data from the blockchain.\nAs long as the flag is equal to `false`, no withdrawals are made.\nAlso returns last scanned workchain block `gen_utime` (unix time) to evaluate the processor's lag from the blockchain.\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n              \"is_synced\": false,\n              \"last_block_gen_utime\": 1718490850,\n            }\n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n## Get income by transaction hash [GET /v1/deposit/income{?tx_hash}]\n\nFind income for deposit by `tx_hash`. Returns the currency and the deposit replenishment with the\nsender's address as `source_address` (if it could be determined).\n\n+ Parameters\n\n    + tx_hash (string) - an unique hash of replenishment transaction\n\n+ Request\n\n    + Headers\n\n            Authorization: Bearer\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n              \"currency\": \"TON\",\n              \"income\":\n                {\n                  \"deposit_address\": \"0QB7BSerVyP9xAKnxp3QpqR8JO2HKwZhl10zsfwg7aJ281ZR\",\n                  \"time\": 1680604643,\n                  \"source_address\": \"0QAUuul9LdYcyJuBHernHo3JkbWTduH_FuEb2H8jCDdGesOP\",\n                  \"amount\": \"1000000\",\n                  \"comment\": \"hello\",\n                  \"tx_hash\": \"9d0fb69b1ca9371bc9e5260a248cda12a1c42916f9051fe9fc21b4abdd41d744\"\n                }\n            }\n\n+ Response 400 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 401\n\n+ Response 404 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n## Resolve domain name [GET /v1/resolve{?domain}]\n\nResolve domain name. Returns bounceable user-friendly address for smart-contract (`dns_smc_address#9fd3` DNS record).\n\n+ Parameters\n\n    + domain (string) - domain name (for example: wallet.ton)\n\n+ Request\n\n    + Headers\n\n            Authorization: Bearer\n\n+ Response 200 (application/json)\n\n    + Body\n\n            {\n              \"address\": \"kQCKt2WPGX-fh0cIAz38Ljd_OKQjoZE_cqk7QrYGsNP6wfP0\"\n            }\n\n+ Response 400 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 401\n\n+ Response 404 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }\n\n+ Response 500 (application/json)\n\n    + Body\n\n            {\n                \"error\": \"error text\"\n            }"
  },
  {
    "path": "docs/index.html",
    "content": "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Payment processor API</title><link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css\"><style>@import url('https://fonts.googleapis.com/css?family=Roboto:400,700|Inconsolata|Raleway:200');.hljs-comment,.hljs-title{color:#8e908c}.hljs-variable,.hljs-attribute,.hljs-tag,.hljs-regexp,.ruby .hljs-constant,.xml .hljs-tag .hljs-title,.xml .hljs-pi,.xml .hljs-doctype,.html .hljs-doctype,.css .hljs-id,.css .hljs-class,.css .hljs-pseudo{color:#c82829}.hljs-number,.hljs-preprocessor,.hljs-pragma,.hljs-built_in,.hljs-literal,.hljs-params,.hljs-constant{color:#f5871f}.ruby .hljs-class .hljs-title,.css .hljs-rules .hljs-attribute{color:#eab700}.hljs-string,.hljs-value,.hljs-inheritance,.hljs-header,.ruby .hljs-symbol,.xml .hljs-cdata{color:#718c00}.css .hljs-hexcolor{color:#3e999f}.hljs-function,.python .hljs-decorator,.python .hljs-title,.ruby .hljs-function .hljs-title,.ruby .hljs-title .hljs-keyword,.perl .hljs-sub,.javascript .hljs-title,.coffeescript .hljs-title{color:#4271ae}.hljs-keyword,.javascript .hljs-function{color:#8959a8}.hljs{display:block;background:white;color:#4d4d4c;padding:.5em}.coffeescript .javascript,.javascript .xml,.tex .hljs-formula,.xml .javascript,.xml .vbscript,.xml .css,.xml .hljs-cdata{opacity:.5}.right .hljs-comment{color:#969896}.right .css .hljs-class,.right .css .hljs-id,.right .css .hljs-pseudo,.right .hljs-attribute,.right .hljs-regexp,.right .hljs-tag,.right .hljs-variable,.right .html .hljs-doctype,.right .ruby .hljs-constant,.right .xml .hljs-doctype,.right .xml .hljs-pi,.right .xml .hljs-tag .hljs-title{color:#c66}.right .hljs-built_in,.right .hljs-constant,.right .hljs-literal,.right .hljs-number,.right .hljs-params,.right .hljs-pragma,.right .hljs-preprocessor{color:#de935f}.right .css .hljs-rule .hljs-attribute,.right .ruby .hljs-class .hljs-title{color:#f0c674}.right .hljs-header,.right .hljs-inheritance,.right .hljs-name,.right .hljs-string,.right .hljs-value,.right .ruby .hljs-symbol,.right .xml .hljs-cdata{color:#b5bd68}.right .css .hljs-hexcolor,.right .hljs-title{color:#8abeb7}.right .coffeescript .hljs-title,.right .hljs-function,.right .javascript .hljs-title,.right .perl .hljs-sub,.right .python .hljs-decorator,.right .python .hljs-title,.right .ruby .hljs-function .hljs-title,.right .ruby .hljs-title .hljs-keyword{color:#81a2be}.right .hljs-keyword,.right .javascript .hljs-function{color:#b294bb}.right .hljs{display:block;overflow-x:auto;background:#1d1f21;color:#c5c8c6;padding:.5em;-webkit-text-size-adjust:none}.right .coffeescript .javascript,.right .javascript .xml,.right .tex .hljs-formula,.right .xml .css,.right .xml .hljs-cdata,.right .xml .javascript,.right .xml .vbscript{opacity:.5}body{color:black;background:white;font:400 14px / 1.42 'Roboto',Helvetica,sans-serif}header{border-bottom:1px solid #f2f2f2;margin-bottom:12px}h1,h2,h3,h4,h5{color:black;margin:12px 0}h1 .permalink,h2 .permalink,h3 .permalink,h4 .permalink,h5 .permalink{margin-left:0;opacity:0;transition:opacity .25s ease}h1:hover .permalink,h2:hover .permalink,h3:hover .permalink,h4:hover .permalink,h5:hover .permalink{opacity:1}.triple h1 .permalink,.triple h2 .permalink,.triple h3 .permalink,.triple h4 .permalink,.triple h5 .permalink{opacity:.15}.triple h1:hover .permalink,.triple h2:hover .permalink,.triple h3:hover .permalink,.triple h4:hover .permalink,.triple h5:hover .permalink{opacity:.15}h1{font:200 36px 'Raleway',Helvetica,sans-serif;font-size:36px}h2{font:200 36px 'Raleway',Helvetica,sans-serif;font-size:30px}h3{font-size:100%;text-transform:uppercase}h5{font-size:100%;font-weight:normal}p{margin:0 0 10px}p.choices{line-height:1.6}a{color:#428bca;text-decoration:none}li p{margin:0}hr.split{border:0;height:1px;width:100%;padding-left:6px;margin:12px auto;background-image:linear-gradient(to right, rgba(0,0,0,0) 20%, rgba(0,0,0,0.2) 51.4%, rgba(255,255,255,0.2) 51.4%, rgba(255,255,255,0) 80%)}dl dt{float:left;width:130px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:700}dl dd{margin-left:150px}blockquote{color:rgba(0,0,0,0.5);font-size:15.5px;padding:10px 20px;margin:12px 0;border-left:5px solid #e8e8e8}blockquote p:last-child{margin-bottom:0}pre{background-color:#f5f5f5;padding:12px;border:1px solid #cfcfcf;border-radius:6px;overflow:auto}pre code{color:black;background-color:transparent;padding:0;border:none}code{color:#444;background-color:#f5f5f5;font:'Inconsolata',monospace;padding:1px 4px;border:1px solid #cfcfcf;border-radius:3px}ul,ol{padding-left:2em}table{border-collapse:collapse;border-spacing:0;margin-bottom:12px}table tr:nth-child(2n){background-color:#fafafa}table th,table td{padding:6px 12px;border:1px solid #e6e6e6}.text-muted{opacity:.5}.note,.warning{padding:.3em 1em;margin:1em 0;border-radius:2px;font-size:90%}.note h1,.warning h1,.note h2,.warning h2,.note h3,.warning h3,.note h4,.warning h4,.note h5,.warning h5,.note h6,.warning h6{font-family:200 36px 'Raleway',Helvetica,sans-serif;font-size:135%;font-weight:500}.note p,.warning p{margin:.5em 0}.note{color:black;background-color:#f0f6fb;border-left:4px solid #428bca}.note h1,.note h2,.note h3,.note h4,.note h5,.note h6{color:#428bca}.warning{color:black;background-color:#fbf1f0;border-left:4px solid #c9302c}.warning h1,.warning h2,.warning h3,.warning h4,.warning h5,.warning h6{color:#c9302c}header{margin-top:24px}nav{position:fixed;top:24px;bottom:0;overflow-y:auto}nav .resource-group{padding:0}nav .resource-group .heading{position:relative}nav .resource-group .heading .chevron{position:absolute;top:12px;right:12px;opacity:.5}nav .resource-group .heading a{display:block;color:black;opacity:.7;border-left:2px solid transparent;margin:0}nav .resource-group .heading a:hover{text-decoration:none;background-color:bad-color;border-left:2px solid black}nav ul{list-style-type:none;padding-left:0}nav ul a{display:block;font-size:13px;color:rgba(0,0,0,0.7);padding:8px 12px;border-top:1px solid #d9d9d9;border-left:2px solid transparent;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nav ul a:hover{text-decoration:none;background-color:bad-color;border-left:2px solid black}nav ul>li{margin:0}nav ul>li:first-child{margin-top:-12px}nav ul>li:last-child{margin-bottom:-12px}nav ul ul a{padding-left:24px}nav ul ul li{margin:0}nav ul ul li:first-child{margin-top:0}nav ul ul li:last-child{margin-bottom:0}nav>div>div>ul>li:first-child>a{border-top:none}.preload *{transition:none !important}.pull-left{float:left}.pull-right{float:right}.badge{display:inline-block;float:right;min-width:10px;min-height:14px;padding:3px 7px;font-size:12px;color:#000;background-color:#f2f2f2;border-radius:10px;margin:-2px 0}.badge.get{color:#70bbe1;background-color:#d9edf7}.badge.head{color:#70bbe1;background-color:#d9edf7}.badge.options{color:#70bbe1;background-color:#d9edf7}.badge.put{color:#f0db70;background-color:#fcf8e3}.badge.patch{color:#f0db70;background-color:#fcf8e3}.badge.post{color:#93cd7c;background-color:#dff0d8}.badge.delete{color:#ce8383;background-color:#f2dede}.collapse-button{float:right}.collapse-button .close{display:none;color:#428bca;cursor:pointer}.collapse-button .open{color:#428bca;cursor:pointer}.collapse-button.show .close{display:inline}.collapse-button.show .open{display:none}.collapse-content{max-height:0;overflow:hidden;transition:max-height .3s ease-in-out}nav{width:220px}.container{max-width:940px;margin-left:auto;margin-right:auto}.container .row .content{margin-left:244px;width:696px}.container .row:after{content:'';display:block;clear:both}.container-fluid nav{width:22%}.container-fluid .row .content{margin-left:24%}.container-fluid.triple nav{width:16.5%;padding-right:1px}.container-fluid.triple .row .content{position:relative;margin-left:16.5%;padding-left:24px}.middle:before,.middle:after{content:'';display:table}.middle:after{clear:both}.middle{box-sizing:border-box;width:51.5%;padding-right:12px}.right{box-sizing:border-box;float:right;width:48.5%;padding-left:12px}.right a{color:#428bca}.right h1,.right h2,.right h3,.right h4,.right h5,.right p,.right div{color:white}.right pre{background-color:#1d1f21;border:1px solid #1d1f21}.right pre code{color:#c5c8c6}.right .description{margin-top:12px}.triple .resource-heading{font-size:125%}.definition{margin-top:12px;margin-bottom:12px}.definition .method{font-weight:bold}.definition .method.get{color:#2e8ab8}.definition .method.head{color:#2e8ab8}.definition .method.options{color:#2e8ab8}.definition .method.post{color:#56b82e}.definition .method.put{color:#b8a22e}.definition .method.patch{color:#b8a22e}.definition .method.delete{color:#b82e2e}.definition .uri{word-break:break-all;word-wrap:break-word}.definition .hostname{opacity:.5}.example-names{background-color:#eee;padding:12px;border-radius:6px}.example-names .tab-button{cursor:pointer;color:black;border:1px solid #ddd;padding:6px;margin-left:12px}.example-names .tab-button.active{background-color:#d5d5d5}.right .example-names{background-color:#444}.right .example-names .tab-button{color:white;border:1px solid #666;border-radius:6px}.right .example-names .tab-button.active{background-color:#5e5e5e}#nav-background{position:fixed;left:0;top:0;bottom:0;width:16.5%;padding-right:14.4px;background-color:#fbfbfb;border-right:1px solid #f0f0f0;z-index:-1}#right-panel-background{position:absolute;right:-12px;top:-12px;bottom:-12px;width:48.6%;background-color:#333;z-index:-1}@media (max-width:1200px){nav{width:198px}.container{max-width:840px}.container .row .content{margin-left:224px;width:606px}}@media (max-width:992px){nav{width:169.4px}.container{max-width:720px}.container .row .content{margin-left:194px;width:526px}}@media (max-width:768px){nav{display:none}.container{width:95%;max-width:none}.container .row .content,.container-fluid .row .content,.container-fluid.triple .row .content{margin-left:auto;margin-right:auto;width:95%}#nav-background{display:none}#right-panel-background{width:48.6%}}.back-to-top{position:fixed;z-index:1;bottom:0;right:24px;padding:4px 8px;color:rgba(0,0,0,0.5);background-color:#f2f2f2;text-decoration:none !important;border-top:1px solid #d9d9d9;border-left:1px solid #d9d9d9;border-right:1px solid #d9d9d9;border-top-left-radius:3px;border-top-right-radius:3px}.resource-group{padding:12px;margin-bottom:12px;background-color:white;border:1px solid #d9d9d9;border-radius:6px}.resource-group h2.group-heading,.resource-group .heading a{padding:12px;margin:-12px -12px 12px -12px;background-color:#f2f2f2;border-bottom:1px solid #d9d9d9;border-top-left-radius:6px;border-top-right-radius:6px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.triple .content .resource-group{padding:0;border:none}.triple .content .resource-group h2.group-heading,.triple .content .resource-group .heading a{margin:0 0 12px 0;border:1px solid #d9d9d9}nav .resource-group .heading a{padding:12px;margin-bottom:0}nav .resource-group .collapse-content{padding:0}.action{margin-bottom:12px;padding:12px 12px 0 12px;overflow:hidden;border:1px solid transparent;border-radius:6px}.action h4.action-heading{padding:6px 12px;margin:-12px -12px 12px -12px;border-bottom:1px solid transparent;border-top-left-radius:6px;border-top-right-radius:6px;overflow:hidden}.action h4.action-heading .name{float:right;font-weight:normal;padding:6px 0}.action h4.action-heading .method{padding:6px 12px;margin-right:12px;border-radius:3px;display:inline-block}.action h4.action-heading .method.get{color:#fff;background-color:#337ab7}.action h4.action-heading .method.head{color:#fff;background-color:#337ab7}.action h4.action-heading .method.options{color:#fff;background-color:#337ab7}.action h4.action-heading .method.put{color:#fff;background-color:#ed9c28}.action h4.action-heading .method.patch{color:#fff;background-color:#ed9c28}.action h4.action-heading .method.post{color:#fff;background-color:#5cb85c}.action h4.action-heading .method.delete{color:#fff;background-color:#d9534f}.action h4.action-heading code{color:#444;background-color:#f5f5f5;border-color:#cfcfcf;font-weight:normal;word-break:break-all;display:inline-block;margin-top:2px}.action dl.inner{padding-bottom:2px}.action .title{border-bottom:1px solid white;margin:0 -12px -1px -12px;padding:12px}.action.get{border-color:#bce8f1}.action.get h4.action-heading{color:#337ab7;background:#d9edf7;border-bottom-color:#bce8f1}.action.head{border-color:#bce8f1}.action.head h4.action-heading{color:#337ab7;background:#d9edf7;border-bottom-color:#bce8f1}.action.options{border-color:#bce8f1}.action.options h4.action-heading{color:#337ab7;background:#d9edf7;border-bottom-color:#bce8f1}.action.post{border-color:#d6e9c6}.action.post h4.action-heading{color:#5cb85c;background:#dff0d8;border-bottom-color:#d6e9c6}.action.put{border-color:#faebcc}.action.put h4.action-heading{color:#ed9c28;background:#fcf8e3;border-bottom-color:#faebcc}.action.patch{border-color:#faebcc}.action.patch h4.action-heading{color:#ed9c28;background:#fcf8e3;border-bottom-color:#faebcc}.action.delete{border-color:#ebccd1}.action.delete h4.action-heading{color:#d9534f;background:#f2dede;border-bottom-color:#ebccd1}</style></head><body class=\"preload\"><a href=\"#top\" class=\"text-muted back-to-top\"><i class=\"fa fa-toggle-up\"></i>&nbsp;Back to top</a><div class=\"container\"><div class=\"row\"><nav><div class=\"resource-group\"><div class=\"heading\"><div class=\"chevron\"><i class=\"open fa fa-angle-down\"></i></div><a href=\"#\">Resource Group</a></div><div class=\"collapse-content\"><ul><li><a href=\"#new-address-post\"><span class=\"badge post\"><i class=\"fa fa-plus\"></i></span>New address</a></li><li><a href=\"#get-all-addresses-get\"><span class=\"badge get\"><i class=\"fa fa-arrow-down\"></i></span>Get all addresses</a></li><li><a href=\"#get-income-get\"><span class=\"badge get\"><i class=\"fa fa-arrow-down\"></i></span>Get income</a></li><li><a href=\"#get-history-get\"><span class=\"badge get\"><i class=\"fa fa-arrow-down\"></i></span>Get history</a></li><li><a href=\"#send-withdrawal-post\"><span class=\"badge post\"><i class=\"fa fa-plus\"></i></span>Send withdrawal</a></li><li><a href=\"#send-service-withdrawal-post\"><span class=\"badge post\"><i class=\"fa fa-plus\"></i></span>Send service withdrawal</a></li><li><a href=\"#send-service-withdrawal-post-1\"><span class=\"badge post\"><i class=\"fa fa-plus\"></i></span>Send service withdrawal</a></li><li><a href=\"#get-withdrawal-status-get\"><span class=\"badge get\"><i class=\"fa fa-arrow-down\"></i></span>Get withdrawal status</a></li><li><a href=\"#get-balance-get\"><span class=\"badge get\"><i class=\"fa fa-arrow-down\"></i></span>Get balance</a></li><li><a href=\"#get-sync-flag-get\"><span class=\"badge get\"><i class=\"fa fa-arrow-down\"></i></span>Get sync flag</a></li><li><a href=\"#get-income-by-transaction-hash-get\"><span class=\"badge get\"><i class=\"fa fa-arrow-down\"></i></span>Get income by transaction hash</a></li><li><a href=\"#resolve-domain-name-get\"><span class=\"badge get\"><i class=\"fa fa-arrow-down\"></i></span>Resolve domain name</a></li></ul></div></div></nav><div class=\"content\"><header><h1 id=\"top\">Payment processor API</h1></header><p>This API describes endpoints of payment processor.</p>\n<section id=\"\" class=\"resource-group\"><h2 class=\"group-heading\">Resource Group <a href=\"#\" class=\"permalink\">&para;</a></h2><div id=\"new-address\" class=\"resource\"><h3 class=\"resource-heading\">New address <a href=\"#new-address\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"new-address-post\" class=\"action post\"><h4 class=\"action-heading\"><div class=\"name\">New address</div><a href=\"#new-address-post\" class=\"method post\">POST</a><code class=\"uri\">/v1/address/new</code></h4><p>Generates new deposit address</p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method post\">POST</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/address/new</span></div><div class=\"title\"><strong>Request</strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span><br><span class=\"hljs-attribute\">Authorization</span>: <span class=\"hljs-string\">Bearer</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">user_id</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"123\"</span></span>,\n  \"<span class=\"hljs-attribute\">currency</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"TON\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QB7BSerVyP9xAKnxp3QpqR8JO2HKwZhl10zsfwg7aJ281ZR\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>400</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>401</code></strong></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div><div id=\"get-all-addresses\" class=\"resource\"><h3 class=\"resource-heading\">Get all addresses <a href=\"#get-all-addresses\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"get-all-addresses-get\" class=\"action get\"><h4 class=\"action-heading\"><div class=\"name\">Get all addresses</div><a href=\"#get-all-addresses-get\" class=\"method get\">GET</a><code class=\"uri\">/v1/address/all{?user_id}</code></h4><p>Get all created addresses by <code>user_id</code></p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method get\">GET</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/address/all?<span class=\"hljs-attribute\">user_id=</span><span class=\"hljs-literal\"></span></span></div><div class=\"title\"><strong>URI Parameters</strong><div class=\"collapse-button show\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><dl class=\"inner\"><dt>user_id</dt><dd><code>string</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>an unique identifier of the user</p>\n</dd></dl></div><div class=\"title\"><strong>Request</strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Authorization</span>: <span class=\"hljs-string\">Bearer</span></code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">addresses</span>\": <span class=\"hljs-value\">[\n    {\n      \"<span class=\"hljs-attribute\">address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QB7BSerVyP9xAKnxp3QpqR8JO2HKwZhl10zsfwg7aJ281ZR\"</span></span>,\n      \"<span class=\"hljs-attribute\">currency</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"TON\"</span>\n    </span>},\n    {\n      \"<span class=\"hljs-attribute\">address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseHynZ_YiH9Y1oSe\"</span></span>,\n      \"<span class=\"hljs-attribute\">currency</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"TGR\"</span>\n    </span>}\n  ]\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>400</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>401</code></strong></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div><div id=\"get-income\" class=\"resource\"><h3 class=\"resource-heading\">Get income <a href=\"#get-income\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"get-income-get\" class=\"action get\"><h4 class=\"action-heading\"><div class=\"name\">Get income</div><a href=\"#get-income-get\" class=\"method get\">GET</a><code class=\"uri\">/v1/income{?user_id}</code></h4><p>Get income for deposits by <code>user_id</code>. The total amount of funds that came to the deposit for the entire time.\nFunds sent from the deposit to the hot wallet or all funds received to the deposit are taken into account,\ndepending on the service settings.\nCounting side field options: “hot_wallet”, “deposit”.</p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method get\">GET</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/income?<span class=\"hljs-attribute\">user_id=</span><span class=\"hljs-literal\"></span></span></div><div class=\"title\"><strong>URI Parameters</strong><div class=\"collapse-button show\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><dl class=\"inner\"><dt>user_id</dt><dd><code>string</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>an unique identifier of the user</p>\n</dd></dl></div><div class=\"title\"><strong>Request</strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Authorization</span>: <span class=\"hljs-string\">Bearer</span></code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">counting_side</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"deposit\"</span></span>,\n  \"<span class=\"hljs-attribute\">total_income</span>\": <span class=\"hljs-value\">[\n    {\n      \"<span class=\"hljs-attribute\">deposit_address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QB7BSerVyP9xAKnxp3QpqR8JO2HKwZhl10zsfwg7aJ281ZR\"</span></span>,\n      \"<span class=\"hljs-attribute\">amount</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"1000000\"</span></span>,\n      \"<span class=\"hljs-attribute\">currency</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"TON\"</span>\n    </span>},\n    {\n      \"<span class=\"hljs-attribute\">deposit_address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseHynZ_YiH9Y1oSe\"</span></span>,\n      \"<span class=\"hljs-attribute\">amount</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"1023000\"</span></span>,\n      \"<span class=\"hljs-attribute\">currency</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"TGR\"</span>\n    </span>}\n  ]\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>400</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>401</code></strong></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div><div id=\"get-history\" class=\"resource\"><h3 class=\"resource-heading\">Get history <a href=\"#get-history\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"get-history-get\" class=\"action get\"><h4 class=\"action-heading\"><div class=\"name\">Get history</div><a href=\"#get-history-get\" class=\"method get\">GET</a><code class=\"uri\">/v1/deposit/history{?user_id,currency,limit,offset,sort_order}</code></h4><p>Get history for deposits by <code>user_id</code> and <code>currency</code>. Returns the history of all deposits replenishments with the\nsender’s address as <code>source_address</code> (if it could be determined).</p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method get\">GET</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/deposit/history?<span class=\"hljs-attribute\">user_id=</span><span class=\"hljs-literal\"></span>&<span class=\"hljs-attribute\">currency=</span><span class=\"hljs-literal\"></span>&<span class=\"hljs-attribute\">limit=</span><span class=\"hljs-literal\"></span>&<span class=\"hljs-attribute\">offset=</span><span class=\"hljs-literal\"></span>&<span class=\"hljs-attribute\">sort_order=</span><span class=\"hljs-literal\"></span></span></div><div class=\"title\"><strong>URI Parameters</strong><div class=\"collapse-button show\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><dl class=\"inner\"><dt>user_id</dt><dd><code>string</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>an unique identifier of the user</p>\n</dd><dt>currency</dt><dd><code>string</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>the text identifier of the currency specified in the processor configuration. <code>TON</code> for TON coin.</p>\n</dd><dt>limit</dt><dd><code>number</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>the maximum value of returned records</p>\n</dd><dt>offset</dt><dd><code>number</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>offset for returned records</p>\n</dd><dt>sort_order</dt><dd><code>string</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>asc or desc. desc by default if the parameter is not specified.</p>\n</dd></dl></div><div class=\"title\"><strong>Request</strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Authorization</span>: <span class=\"hljs-string\">Bearer</span></code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">incomes</span>\": <span class=\"hljs-value\">[\n    {\n      \"<span class=\"hljs-attribute\">deposit_address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QB7BSerVyP9xAKnxp3QpqR8JO2HKwZhl10zsfwg7aJ281ZR\"</span></span>,\n      \"<span class=\"hljs-attribute\">time</span>\": <span class=\"hljs-value\"><span class=\"hljs-number\">1680604643</span></span>,\n      \"<span class=\"hljs-attribute\">source_address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QAUuul9LdYcyJuBHernHo3JkbWTduH_FuEb2H8jCDdGesOP\"</span></span>,\n      \"<span class=\"hljs-attribute\">amount</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"1000000\"</span></span>,\n      \"<span class=\"hljs-attribute\">comment</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"hello\"</span></span>,\n      \"<span class=\"hljs-attribute\">tx_hash</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"9d0fb69b1ca9371bc9e5260a248cda12a1c42916f9051fe9fc21b4abdd41d744\"</span>\n    </span>},\n    {\n      \"<span class=\"hljs-attribute\">deposit_address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseHynZ_YiH9Y1oSe\"</span></span>,\n      \"<span class=\"hljs-attribute\">time</span>\": <span class=\"hljs-value\"><span class=\"hljs-number\">1680604648</span></span>,\n      \"<span class=\"hljs-attribute\">source_address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QAUuul9LdYcyJuBHernHo3JkbWTduH_FuEb2H8jCDdGesOP\"</span></span>,\n      \"<span class=\"hljs-attribute\">amount</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"1000000\"</span></span>,\n      \"<span class=\"hljs-attribute\">comment</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"hello\"</span></span>,\n      \"<span class=\"hljs-attribute\">tx_hash</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"7d0fb69b1ca9371bc9e5260a248cda12a1c42916f9051fe9fc21b4abdd41d744\"</span>\n    </span>}\n  ]\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>400</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>401</code></strong></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div><div id=\"send-withdrawal\" class=\"resource\"><h3 class=\"resource-heading\">Send withdrawal <a href=\"#send-withdrawal\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"send-withdrawal-post\" class=\"action post\"><h4 class=\"action-heading\"><div class=\"name\">Send withdrawal</div><a href=\"#send-withdrawal-post\" class=\"method post\">POST</a><code class=\"uri\">/v1/withdrawal/send</code></h4><p>Send withdrawal request. <strong>Amount must be in base units without decimal point (NanoTONs for TONs)</strong>\nInstead of a <code>comment</code>, you can specify a <code>binary_comment</code> in hex notation format.\n<code>binary_comment</code> supports binary string hex representation form with flip bit (example: <code>9fe7_</code>)</p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method post\">POST</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/withdrawal/send</span></div><div class=\"title\"><strong>Request</strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span><br><span class=\"hljs-attribute\">Authorization</span>: <span class=\"hljs-string\">Bearer</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">user_id</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"123\"</span></span>,\n  \"<span class=\"hljs-attribute\">query_id</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"321\"</span></span>,\n  \"<span class=\"hljs-attribute\">currency</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"TON\"</span></span>,\n  \"<span class=\"hljs-attribute\">amount</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"100\"</span></span>,\n  \"<span class=\"hljs-attribute\">destination</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseHynZ_YiH9Y1oSe\"</span></span>,\n  \"<span class=\"hljs-attribute\">comment</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"hello\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">ID</span>\": <span class=\"hljs-value\"><span class=\"hljs-number\">1</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>400</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>401</code></strong></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div><div id=\"send-service-withdrawal\" class=\"resource\"><h3 class=\"resource-heading\">Send service withdrawal <a href=\"#send-service-withdrawal\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"send-service-withdrawal-post\" class=\"action post\"><h4 class=\"action-heading\"><div class=\"name\">Send service withdrawal</div><a href=\"#send-service-withdrawal-post\" class=\"method post\">POST</a><code class=\"uri\">/v1/withdrawal/service/ton</code></h4><p>Send service withdrawal request. Withdraw all TONs from <code>from</code> address to hot wallet.\nReturns <code>memo</code> as comment for transfer message.</p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method post\">POST</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/withdrawal/service/ton</span></div><div class=\"title\"><strong>Request</strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span><br><span class=\"hljs-attribute\">Authorization</span>: <span class=\"hljs-string\">Bearer</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">from</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QAUuul9LdYcyJuBHernHo3JkbWTduH_FuEb2H8jCDdGesOP\"</span></span>,\n}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">memo</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"123e4567-e89b-12d3-a456-426655440000\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>400</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>401</code></strong></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div><div id=\"send-service-withdrawal-1\" class=\"resource\"><h3 class=\"resource-heading\">Send service withdrawal <a href=\"#send-service-withdrawal-1\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"send-service-withdrawal-post-1\" class=\"action post\"><h4 class=\"action-heading\"><div class=\"name\">Send service withdrawal</div><a href=\"#send-service-withdrawal-post-1\" class=\"method post\">POST</a><code class=\"uri\">/v1/withdrawal/service/jetton</code></h4><p>Send service withdrawal request. Withdraw all Jettons from Jetton wallet. Address calculated through owner and Jetton master.\nReturns <code>memo</code> as comment for transfer message.</p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method post\">POST</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/withdrawal/service/jetton</span></div><div class=\"title\"><strong>Request</strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span><br><span class=\"hljs-attribute\">Authorization</span>: <span class=\"hljs-string\">Bearer</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">owner</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QAUuul9LdYcyJuBHernHo3JkbWTduH_FuEb2H8jCDdGesOP\"</span></span>,\n  \"<span class=\"hljs-attribute\">jetton_master</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"kQCKt2WPGX-fh0cIAz38Ljd_OKQjoZE_cqk7QrYGsNP6wfP0\"</span></span>,\n}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">memo</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"123e4567-e89b-12d3-a456-426655440000\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>400</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>401</code></strong></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div><div id=\"get-withdrawal-status\" class=\"resource\"><h3 class=\"resource-heading\">Get withdrawal status <a href=\"#get-withdrawal-status\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"get-withdrawal-status-get\" class=\"action get\"><h4 class=\"action-heading\"><div class=\"name\">Get withdrawal status</div><a href=\"#get-withdrawal-status-get\" class=\"method get\">GET</a><code class=\"uri\">/v1/withdrawal/status{?id}</code></h4><p>Get withdrawal status. Returns <code>pending</code>, <code>processing</code>, <code>processed</code>/‘failed’, transaction hash for processed withdrawal and request meta (user_id and query_id).</p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method get\">GET</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/withdrawal/status?<span class=\"hljs-attribute\">id=</span><span class=\"hljs-literal\"></span></span></div><div class=\"title\"><strong>URI Parameters</strong><div class=\"collapse-button show\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><dl class=\"inner\"><dt>id</dt><dd><code>number</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>An unique identifier of the withdrawal.</p>\n</dd></dl></div><div class=\"title\"><strong>Request</strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span><br><span class=\"hljs-attribute\">Authorization</span>: <span class=\"hljs-string\">Bearer</span></code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">user_id</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"123\"</span></span>,\n  \"<span class=\"hljs-attribute\">query_id</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"321\"</span></span>,\n  \"<span class=\"hljs-attribute\">status</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"processed\"</span></span>,\n  \"<span class=\"hljs-attribute\">tx_hash</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"9d0fb69b1ca9371bc9e5260a248cda12a1c42916f9051fe9fc21b4abdd41d744\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>400</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>401</code></strong></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div><div id=\"get-balance\" class=\"resource\"><h3 class=\"resource-heading\">Get balance <a href=\"#get-balance\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"get-balance-get\" class=\"action get\"><h4 class=\"action-heading\"><div class=\"name\">Get balance</div><a href=\"#get-balance-get\" class=\"method get\">GET</a><code class=\"uri\">/v1/balance{?currency,address}</code></h4><p>Get balance for account by <code>address</code> and <code>currency</code>. Returns the balance of hot wallet if <code>address</code> not specified.\nFor hot wallet also returns <code>total_processing_amount</code> and <code>total_pending_amount</code> for withdrawals in queue.\nFor TON also returns account status: “active”, “uninit”, “frozen”, “non_exist”.</p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method get\">GET</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/balance?<span class=\"hljs-attribute\">currency=</span><span class=\"hljs-literal\"></span>&<span class=\"hljs-attribute\">address=</span><span class=\"hljs-literal\"></span></span></div><div class=\"title\"><strong>URI Parameters</strong><div class=\"collapse-button show\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><dl class=\"inner\"><dt>currency</dt><dd><code>string</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>the text identifier of the currency specified in the processor configuration. <code>TON</code> for TON coin.</p>\n</dd><dt>address</dt><dd><code>string</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>address in URL-safe, user-friendly Base64 form. Hot wallet address by default if the parameter is not specified.</p>\n</dd></dl></div><div class=\"title\"><strong>Request</strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Authorization</span>: <span class=\"hljs-string\">Bearer</span></code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">balance</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"1000000\"</span></span>,\n  \"<span class=\"hljs-attribute\">status</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"active\"</span></span>,\n  \"<span class=\"hljs-attribute\">total_processing_amount</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"1000\"</span></span>,\n  \"<span class=\"hljs-attribute\">total_pending_amount</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"1000\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>400</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>401</code></strong></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div><div id=\"get-sync-flag\" class=\"resource\"><h3 class=\"resource-heading\">Get sync flag <a href=\"#get-sync-flag\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"get-sync-flag-get\" class=\"action get\"><h4 class=\"action-heading\"><div class=\"name\">Get sync flag</div><a href=\"#get-sync-flag-get\" class=\"method get\">GET</a><code class=\"uri\">/v1/system/sync</code></h4><p>Get blockchain sync flag. Returns <code>true</code> if the service has up-to-date data from the blockchain.\nAs long as the flag is equal to <code>false</code>, no withdrawals are made.\nAlso returns last scanned workchain block <code>gen_utime</code> (unix time) to evaluate the processor’s lag from the blockchain.</p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method get\">GET</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/system/sync</span></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">is_synced</span>\": <span class=\"hljs-value\"><span class=\"hljs-literal\">false</span></span>,\n  \"<span class=\"hljs-attribute\">last_block_gen_utime</span>\": <span class=\"hljs-value\"><span class=\"hljs-number\">1718490850</span></span>,\n}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div><div id=\"get-income-by-transaction-hash\" class=\"resource\"><h3 class=\"resource-heading\">Get income by transaction hash <a href=\"#get-income-by-transaction-hash\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"get-income-by-transaction-hash-get\" class=\"action get\"><h4 class=\"action-heading\"><div class=\"name\">Get income by transaction hash</div><a href=\"#get-income-by-transaction-hash-get\" class=\"method get\">GET</a><code class=\"uri\">/v1/deposit/income{?tx_hash}</code></h4><p>Find income for deposit by <code>tx_hash</code>. Returns the currency and the deposit replenishment with the\nsender’s address as <code>source_address</code> (if it could be determined).</p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method get\">GET</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/deposit/income?<span class=\"hljs-attribute\">tx_hash=</span><span class=\"hljs-literal\"></span></span></div><div class=\"title\"><strong>URI Parameters</strong><div class=\"collapse-button show\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><dl class=\"inner\"><dt>tx_hash</dt><dd><code>string</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>an unique hash of replenishment transaction</p>\n</dd></dl></div><div class=\"title\"><strong>Request</strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Authorization</span>: <span class=\"hljs-string\">Bearer</span></code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">currency</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"TON\"</span></span>,\n  \"<span class=\"hljs-attribute\">income</span>\": <span class=\"hljs-value\">{\n    \"<span class=\"hljs-attribute\">deposit_address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QB7BSerVyP9xAKnxp3QpqR8JO2HKwZhl10zsfwg7aJ281ZR\"</span></span>,\n    \"<span class=\"hljs-attribute\">time</span>\": <span class=\"hljs-value\"><span class=\"hljs-number\">1680604643</span></span>,\n    \"<span class=\"hljs-attribute\">source_address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"0QAUuul9LdYcyJuBHernHo3JkbWTduH_FuEb2H8jCDdGesOP\"</span></span>,\n    \"<span class=\"hljs-attribute\">amount</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"1000000\"</span></span>,\n    \"<span class=\"hljs-attribute\">comment</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"hello\"</span></span>,\n    \"<span class=\"hljs-attribute\">tx_hash</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"9d0fb69b1ca9371bc9e5260a248cda12a1c42916f9051fe9fc21b4abdd41d744\"</span>\n  </span>}\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>400</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>401</code></strong></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>404</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div><div id=\"resolve-domain-name\" class=\"resource\"><h3 class=\"resource-heading\">Resolve domain name <a href=\"#resolve-domain-name\" class=\"permalink\">&nbsp;&para;</a></h3><div id=\"resolve-domain-name-get\" class=\"action get\"><h4 class=\"action-heading\"><div class=\"name\">Resolve domain name</div><a href=\"#resolve-domain-name-get\" class=\"method get\">GET</a><code class=\"uri\">/v1/resolve{?domain}</code></h4><p>Resolve domain name. Returns bounceable user-friendly address for smart-contract (<code>dns_smc_address#9fd3</code> DNS record).</p>\n<h4>Example URI</h4><div class=\"definition\"><span class=\"method get\">GET</span>&nbsp;<span class=\"uri\"><span class=\"hostname\"></span>/v1/resolve?<span class=\"hljs-attribute\">domain=</span><span class=\"hljs-literal\"></span></span></div><div class=\"title\"><strong>URI Parameters</strong><div class=\"collapse-button show\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><dl class=\"inner\"><dt>domain</dt><dd><code>string</code>&nbsp;<span class=\"required\">(required)</span>&nbsp;<p>domain name (for example: wallet.ton)</p>\n</dd></dl></div><div class=\"title\"><strong>Request</strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Authorization</span>: <span class=\"hljs-string\">Bearer</span></code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>200</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">address</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"kQCKt2WPGX-fh0cIAz38Ljd_OKQjoZE_cqk7QrYGsNP6wfP0\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>400</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>401</code></strong></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>404</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div><div class=\"title\"><strong>Response&nbsp;&nbsp;<code>500</code></strong><div class=\"collapse-button\"><span class=\"close\">Hide</span><span class=\"open\">Show</span></div></div><div class=\"collapse-content\"><div class=\"inner\"><h5>Headers</h5><pre><code><span class=\"hljs-attribute\">Content-Type</span>: <span class=\"hljs-string\">application/json</span></code></pre><div style=\"height: 1px;\"></div><h5>Body</h5><pre><code>{\n  \"<span class=\"hljs-attribute\">error</span>\": <span class=\"hljs-value\"><span class=\"hljs-string\">\"error text\"</span>\n</span>}</code></pre><div style=\"height: 1px;\"></div></div></div></div></div></section></div></div></div><p style=\"text-align: center;\" class=\"text-muted\">Generated by&nbsp;<a href=\"https://github.com/danielgtaylor/aglio\" class=\"aglio\">aglio</a>&nbsp;on 17 Jun 2024</p><script>/* eslint-env browser */\n/* eslint quotes: [2, \"single\"] */\n'use strict';\n\n/*\n  Determine if a string ends with another string.\n*/\nfunction endsWith(str, suffix) {\n    return str.indexOf(suffix, str.length - suffix.length) !== -1;\n}\n\n/*\n  Get a list of direct child elements by class name.\n*/\nfunction childrenByClass(element, name) {\n  var filtered = [];\n\n  for (var i = 0; i < element.children.length; i++) {\n    var child = element.children[i];\n    var classNames = child.className.split(' ');\n    if (classNames.indexOf(name) !== -1) {\n      filtered.push(child);\n    }\n  }\n\n  return filtered;\n}\n\n/*\n  Get an array [width, height] of the window.\n*/\nfunction getWindowDimensions() {\n  var w = window,\n      d = document,\n      e = d.documentElement,\n      g = d.body,\n      x = w.innerWidth || e.clientWidth || g.clientWidth,\n      y = w.innerHeight || e.clientHeight || g.clientHeight;\n\n  return [x, y];\n}\n\n/*\n  Collapse or show a request/response example.\n*/\nfunction toggleCollapseButton(event) {\n    var button = event.target.parentNode;\n    var content = button.parentNode.nextSibling;\n    var inner = content.children[0];\n\n    if (button.className.indexOf('collapse-button') === -1) {\n      // Clicked without hitting the right element?\n      return;\n    }\n\n    if (content.style.maxHeight && content.style.maxHeight !== '0px') {\n        // Currently showing, so let's hide it\n        button.className = 'collapse-button';\n        content.style.maxHeight = '0px';\n    } else {\n        // Currently hidden, so let's show it\n        button.className = 'collapse-button show';\n        content.style.maxHeight = inner.offsetHeight + 12 + 'px';\n    }\n}\n\nfunction toggleTabButton(event) {\n    var i, index;\n    var button = event.target;\n\n    // Get index of the current button.\n    var buttons = childrenByClass(button.parentNode, 'tab-button');\n    for (i = 0; i < buttons.length; i++) {\n        if (buttons[i] === button) {\n            index = i;\n            button.className = 'tab-button active';\n        } else {\n            buttons[i].className = 'tab-button';\n        }\n    }\n\n    // Hide other tabs and show this one.\n    var tabs = childrenByClass(button.parentNode.parentNode, 'tab');\n    for (i = 0; i < tabs.length; i++) {\n        if (i === index) {\n            tabs[i].style.display = 'block';\n        } else {\n            tabs[i].style.display = 'none';\n        }\n    }\n}\n\n/*\n  Collapse or show a navigation menu. It will not be hidden unless it\n  is currently selected or `force` has been passed.\n*/\nfunction toggleCollapseNav(event, force) {\n    var heading = event.target.parentNode;\n    var content = heading.nextSibling;\n    var inner = content.children[0];\n\n    if (heading.className.indexOf('heading') === -1) {\n      // Clicked without hitting the right element?\n      return;\n    }\n\n    if (content.style.maxHeight && content.style.maxHeight !== '0px') {\n      // Currently showing, so let's hide it, but only if this nav item\n      // is already selected. This prevents newly selected items from\n      // collapsing in an annoying fashion.\n      if (force || window.location.hash && endsWith(event.target.href, window.location.hash)) {\n        content.style.maxHeight = '0px';\n      }\n    } else {\n      // Currently hidden, so let's show it\n      content.style.maxHeight = inner.offsetHeight + 12 + 'px';\n    }\n}\n\n/*\n  Refresh the page after a live update from the server. This only\n  works in live preview mode (using the `--server` parameter).\n*/\nfunction refresh(body) {\n    document.querySelector('body').className = 'preload';\n    document.body.innerHTML = body;\n\n    // Re-initialize the page\n    init();\n    autoCollapse();\n\n    document.querySelector('body').className = '';\n}\n\n/*\n  Determine which navigation items should be auto-collapsed to show as many\n  as possible on the screen, based on the current window height. This also\n  collapses them.\n*/\nfunction autoCollapse() {\n  var windowHeight = getWindowDimensions()[1];\n  var itemsHeight = 64; /* Account for some padding */\n  var itemsArray = Array.prototype.slice.call(\n    document.querySelectorAll('nav .resource-group .heading'));\n\n  // Get the total height of the navigation items\n  itemsArray.forEach(function (item) {\n    itemsHeight += item.parentNode.offsetHeight;\n  });\n\n  // Should we auto-collapse any nav items? Try to find the smallest item\n  // that can be collapsed to show all items on the screen. If not possible,\n  // then collapse the largest item and do it again. First, sort the items\n  // by height from smallest to largest.\n  var sortedItems = itemsArray.sort(function (a, b) {\n    return a.parentNode.offsetHeight - b.parentNode.offsetHeight;\n  });\n\n  while (sortedItems.length && itemsHeight > windowHeight) {\n    for (var i = 0; i < sortedItems.length; i++) {\n      // Will collapsing this item help?\n      var itemHeight = sortedItems[i].nextSibling.offsetHeight;\n      if ((itemsHeight - itemHeight <= windowHeight) || i === sortedItems.length - 1) {\n        // It will, so let's collapse it, remove its content height from\n        // our total and then remove it from our list of candidates\n        // that can be collapsed.\n        itemsHeight -= itemHeight;\n        toggleCollapseNav({target: sortedItems[i].children[0]}, true);\n        sortedItems.splice(i, 1);\n        break;\n      }\n    }\n  }\n}\n\n/*\n  Initialize the interactive functionality of the page.\n*/\nfunction init() {\n    var i, j;\n\n    // Make collapse buttons clickable\n    var buttons = document.querySelectorAll('.collapse-button');\n    for (i = 0; i < buttons.length; i++) {\n        buttons[i].onclick = toggleCollapseButton;\n\n        // Show by default? Then toggle now.\n        if (buttons[i].className.indexOf('show') !== -1) {\n            toggleCollapseButton({target: buttons[i].children[0]});\n        }\n    }\n\n    var responseCodes = document.querySelectorAll('.example-names');\n    for (i = 0; i < responseCodes.length; i++) {\n        var tabButtons = childrenByClass(responseCodes[i], 'tab-button');\n        for (j = 0; j < tabButtons.length; j++) {\n            tabButtons[j].onclick = toggleTabButton;\n\n            // Show by default?\n            if (j === 0) {\n                toggleTabButton({target: tabButtons[j]});\n            }\n        }\n    }\n\n    // Make nav items clickable to collapse/expand their content.\n    var navItems = document.querySelectorAll('nav .resource-group .heading');\n    for (i = 0; i < navItems.length; i++) {\n        navItems[i].onclick = toggleCollapseNav;\n\n        // Show all by default\n        toggleCollapseNav({target: navItems[i].children[0]});\n    }\n}\n\n// Initial call to set up buttons\ninit();\n\nwindow.onload = function () {\n    autoCollapse();\n    // Remove the `preload` class to enable animations\n    document.querySelector('body').className = '';\n};\n</script></body></html>"
  },
  {
    "path": "go.mod",
    "content": "module github.com/gobicycle/bicycle\n\ngo 1.24.0\n\ntoolchain go1.24.11\n\nrequire (\n\tgithub.com/caarlos0/env/v6 v6.10.1\n\tgithub.com/gofrs/uuid v4.4.0+incompatible\n\tgithub.com/jackc/pgx/v4 v4.18.2\n\tgithub.com/prometheus/client_golang v1.19.1\n\tgithub.com/rabbitmq/amqp091-go v1.10.0\n\tgithub.com/shopspring/decimal v1.4.0\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgithub.com/tonkeeper/tongo v1.16.12\n\tgithub.com/xssnick/tonutils-go v1.13.1\n\tgolang.org/x/time v0.10.0\n)\n\nrequire (\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.2.0 // indirect\n\tgithub.com/jackc/chunkreader/v2 v2.0.1 // indirect\n\tgithub.com/jackc/pgconn v1.14.3 // indirect\n\tgithub.com/jackc/pgio v1.0.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgproto3/v2 v2.3.3 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect\n\tgithub.com/jackc/pgtype v1.14.0 // indirect\n\tgithub.com/jackc/puddle v1.3.0 // indirect\n\tgithub.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae // indirect\n\tgithub.com/prometheus/client_model v0.5.0 // indirect\n\tgithub.com/prometheus/common v0.48.0 // indirect\n\tgithub.com/prometheus/procfs v0.12.0 // indirect\n\tgithub.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect\n\tgithub.com/snksoft/crc v1.1.0 // indirect\n\tgolang.org/x/crypto v0.45.0 // indirect\n\tgolang.org/x/exp v0.0.0-20230116083435-1de6713980de // indirect\n\tgolang.org/x/sys v0.38.0 // indirect\n\tgolang.org/x/text v0.31.0 // indirect\n\tgoogle.golang.org/protobuf v1.33.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II=\ngithub.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=\ngithub.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=\ngithub.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=\ngithub.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=\ngithub.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=\ngithub.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=\ngithub.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=\ngithub.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=\ngithub.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=\ngithub.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=\ngithub.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=\ngithub.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=\ngithub.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=\ngithub.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=\ngithub.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=\ngithub.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=\ngithub.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=\ngithub.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=\ngithub.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=\ngithub.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=\ngithub.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=\ngithub.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=\ngithub.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=\ngithub.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=\ngithub.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=\ngithub.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=\ngithub.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=\ngithub.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=\ngithub.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=\ngithub.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=\ngithub.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU=\ngithub.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=\ngithub.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=\ngithub.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=\ngithub.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae h1:7smdlrfdcZic4VfsGKD2ulWL804a4GVphr4s7WZxGiY=\ngithub.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=\ngithub.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=\ngithub.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=\ngithub.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=\ngithub.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=\ngithub.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=\ngithub.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=\ngithub.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=\ngithub.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=\ngithub.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=\ngithub.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=\ngithub.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=\ngithub.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=\ngithub.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=\ngithub.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/snksoft/crc v1.1.0 h1:HkLdI4taFlgGGG1KvsWMpz78PkOC9TkPVpTV/cuWn48=\ngithub.com/snksoft/crc v1.1.0/go.mod h1:5/gUOsgAm7OmIhb6WJzw7w5g2zfJi4FrHYgGPdshE+A=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/tonkeeper/tongo v1.16.12 h1:oAS9z0Kj7okTwVUV7QKAq3E6XjYmx+6DXfYaP8gg4S4=\ngithub.com/tonkeeper/tongo v1.16.12/go.mod h1:MjgIgAytFarjCoVjMLjYEtpZNN1f2G/pnZhKjr28cWs=\ngithub.com/xssnick/tonutils-go v1.13.1 h1:eWMD3KoRDX29gjAQIcLJU2Lnnzojr97xpzaMOEFjOeE=\ngithub.com/xssnick/tonutils-go v1.13.1/go.mod h1:EDe/9D/HZpAenbR+WPMQHICOF0BZWAe01TU5+Vpg08k=\ngithub.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=\ngo.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=\ngo.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=\ngo.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=\ngo.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=\ngolang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=\ngolang.org/x/exp v0.0.0-20230116083435-1de6713980de h1:DBWn//IJw30uYCgERoxCg84hWtA97F4wMiKOIh00Uf0=\ngolang.org/x/exp v0.0.0-20230116083435-1de6713980de/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=\ngolang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=\ngolang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=\ngolang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=\ngolang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=\ngoogle.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\n"
  },
  {
    "path": "jettons.md",
    "content": "# Jettons compatibility information\n\n**! Be careful with Jettons metadata, the information may not be up-to-date, see the current one in the explorer or https://github.com/tonkeeper/ton-assets**\n\n## TGR\n\n### mainnet\n- Address: 0:2F0DF5851B4A185F5F63C0D0CD0412F5ACA353F577DA18FF47C936F99DBD849A\n- Code hash: 394e236fdaa478d218aa923b06dc0d3ad08ce65cbce5b0ab3ead62c61da69920\n- Metadata:\n  * name: Tegro\n  * symbol: TGR\n  * description: Cross-platform DeFi ecosystem Tegro\n  * image: https://tegro.io/tgr.png\n- Tested with payment processor:\n  - [x] Deposit filling from external address and withdrawal to hot wallet\n  - [x] Withdrawal to new external address with wallet init\n  - [ ] Highload test\n- Presented in https://github.com/tonkeeper/ton-assets: yes\n- Comments: sends notification with bounce flag\n\n### testnet\n- Address: 0:8AB7658F197F9F874708033DFC2E377F38A423A1913F72A93B42B606B0D3FAC1\n- Code hash: failed to calc via tonutils\n- Metadata:\n  * name: Tegro\n  * symbol: TGR\n  * description: Cross-platform payment token on the TON blockchain.\n  * image: https://tegro.io/tgr.png\n- Tested with payment processor:\n  - [x] Deposit filling from external address and withdrawal to hot wallet\n  - [x] Withdrawal to new external address with wallet init\n  - [x] Highload test\n- Presented in https://github.com/tonkeeper/ton-assets: no\n- Comments: code not equal to actual TGR jetton in mainnet! Jetton with the equivalent code was not found in the testnet.\n\n## SCALE\n\n### mainnet\n- Address: 0:65aac9b5e380eae928db3c8e238d9bc0d61a9320fdc2bc7a2f6c87d6fedf9208\n- Code hash: c81828dcd4df2a3a7516762c154dd973b92a26408db3bc143c7adda0576a9d6c\n- Metadata:\n  * name: Scaleton\n  * symbol: SCALE\n  * description: SCALE is a utility token that will be used to support all independent developers.\n  * image: ipfs://QmSMiXsZYMefwrTQ3P6HnDQaCpecS4EWLpgKK5EX1G8iA8\n- Tested with payment processor:\n  - [ ] Deposit filling from external address and withdrawal to hot wallet\n  - [ ] Withdrawal to new external address with wallet init\n  - [ ] Highload test\n- Presented in https://github.com/tonkeeper/ton-assets: yes\n- Comments:\n\n### testnet\n- Address: 0:1b310ceeb868829ded11ed0123f67ad7b2333b80d8de566ae5059a2ec82f9208\n- Code hash: f95ba0330b38cdf3459b1e811e5fc6fa6cfee566d7b764455c0468140365a737\n- Metadata:\n  * name: Scaleton\n  * symbol: SCALE\n  * description:\n  * image: ipfs://QmSMiXsZYMefwrTQ3P6HnDQaCpecS4EWLpgKK5EX1G8iA8\n- Tested with payment processor:\n  - [x] Deposit filling from external address and withdrawal to hot wallet\n  - [x] Withdrawal to new external address with wallet init\n  - [x] Highload test\n- Presented in https://github.com/tonkeeper/ton-assets: no\n- Comments: code not equal to actual SCALE jetton in mainnet!\n\n## FNZ\n\n### mainnet\n- Address: 0:C224BD22407A1F70106F1411DD546DB7DD18B657890234581F453FA0A8077739\n- Code hash: b4804ee49db9823eb5e9bdd98ff3784913c5dd01762dfd6e257f15be97b9fcf4\n- Metadata:\n    * name: Fanzee Token \n    * symbol: FNZ\n    * description: fanz.ee is a web3 fan engagement platform designed to help sports and entertainment organisations meaningfully connect with their fans through immersive gamification experiences\n    * image: https://media.fanz.ee/images/91ee938a92934656a01131c569b377b6.png\n    * decimals: 9\n- Tested with payment processor:\n  - [x] Deposit filling from external address and withdrawal to hot wallet\n  - [x] Withdrawal to new external address with wallet init\n  - [ ] Highload test\n- Presented in https://github.com/tonkeeper/ton-assets: yes\n- Comments: sends notification with non-bounce flag\n\n### testnet\n- Address: 0:FCEE09C7BA28DDBA7B3F83EEBC6EC1B36B3AA3E40B98C3A69372DC1EA711353D\n- Code hash: b4804ee49db9823eb5e9bdd98ff3784913c5dd01762dfd6e257f15be97b9fcf4\n- Metadata:\n    * name: FANZEE test Coin\n    * symbol: FNZT\n    * description: This is an example jetton for the TON network\n    * image: https://media.fanz.ee/images/91ee938a92934656a01131c569b377b6.png\n    * decimals: 9\n- Tested with payment processor:\n  - [ ] Deposit filling from external address and withdrawal to hot wallet\n  - [ ] Withdrawal to new external address with wallet init\n  - [ ] Highload test\n- Presented in https://github.com/tonkeeper/ton-assets: no\n- Comments: code is equal with the mainnet jetton.\n"
  },
  {
    "path": "manual_migrations.md",
    "content": "# Manual migrations between versions\n\n## v0.1.x -> v0.2.0\n1. Apply [DB migration](/deploy/manual_migrations/0.1.x-0.2.0.sql)\n2. Build new docker image and recreate container for `payment-processor` as described in `Service deploy` chapter in [Readme](/README.md)\n\nNote that this query creates a new column in the `external_incomes` DB table. All existing values for the payer address \nwill be filled with a 0 workchain.\n\n## v0.4.x -> v0.5.0\n1. Apply [DB migration](/deploy/manual_migrations/0.4.x-0.5.0.sql)\n2. Build new docker image and recreate container for `payment-processor` as described in `Service deploy` chapter in [Readme](/README.md)\n\nNote that this query creates a new nullable column in the `external_withdrawals` and `external_incomes` DB tables and `binary_comment` column in `withdrawal_requests` table.\n"
  },
  {
    "path": "manual_testing_plan.md",
    "content": "## Manual testing plan for v0.5.0\nTemplate:\n-[x] Checked\n- TEST    : test description\n- RESULT  : expected result\n- COMMENT : some comment \n\n### Initialization\n\n1. -[X] Checked\n- TEST    : Run with not deployed (and zero balance) hot wallet (new seed phrase)\n- RESULT  : There must be an insufficient balance error\n- COMMENT : \n\n2. -[X] Checked\n- TEST    : Run with uninit hot wallet with balance > minimum balance\n- RESULT  : Hot wallet must be initialized at first start of service\n- COMMENT :\n\n3. -[X] Checked\n- TEST    : Run with new seed phrase when hot wallet already exist in DB\n- RESULT  : There must be an incorrect seed phrase error\n- COMMENT :\n\n4. -[X] Checked\n- TEST    : Run service with empty DB and stop after few minutes. Check time of first and last block \n            in `block_data` table\n- RESULT  : Time `saved_at` and `gen_utime` must correlate with system time\n- COMMENT :\n\n5. -[X] Checked\n- TEST    : Run with nonexist hot jetton wallet and receive external jetton transfer at jetton deposit \n            (> minimal withdrawal amount)\n- RESULT  : Jetton hot wallet must be initialized by Jetton withdrawal from deposit, \n            if jetton deposit successfully initialized (it depends on transfer sender)\n- COMMENT :\n\n6. -[X] Checked\n- TEST    : Run with testnet cold wallet address at mainnet (`IS_TESTNET=false`)\n- RESULT  : There must be \"Can not use testnet cold wallet address for mainnet\" error\n- COMMENT :\n\n7. -[X] Checked\n- TEST    : Run service with empty `JETTONS` env variable\n- RESULT  : Service must start and process TONs\n- COMMENT :\n\n8. -[X] Checked\n- TEST    : Run service with `JETTONS` env variable with different currencies and same master contract address.\n            Like `TGR:ABC...,FNZ:ABC...`.\n- RESULT  : Service must stop. Must be address duplication error message in audit log.\n- COMMENT :\n\n9. -[X] Checked\n- TEST    : Run service with one `JETTONS` env variable, then rename currency for one of Jetton and restart.\n            Like `TGR:ABC...,FNZ:CDE...` -> `SCALE:ABC...,FNZ:CDE...`.\n- RESULT  : Service must stop. Must be address duplication error message in audit log.\n- COMMENT :\n\n10. -[X] Checked\n- TEST    : Start service with uninitialized cold wallet and bounceable address for cold wallet.\n- RESULT  : Service must stop. Must be invalid address format error message in log.\n- COMMENT :\n\n11. -[X] Checked\n- TEST    : Start service with `PROOF_CHECK_ENABLED=true` and empty or invalid `NETWORK_CONFIG_URL` ENV variable.\n- RESULT  : Service must stop. Must be blockchain connection error message in log.\n- COMMENT : \n\n12. -[X] Checked\n- TEST    : Start service with `PROOF_CHECK_ENABLED=false` ENV variable.\n- RESULT  : Service must start normally.\n- COMMENT :\n\n13. -[ ] Checked\n- TEST    : Start service with `PROOF_CHECK_ENABLED=true` and valid `NETWORK_CONFIG_URL` ENV variable.\n- RESULT  : Service must start normally. Must be `Proof checks are completed` message in log.\n- COMMENT : Proof check can not get account state for each workchain block. \n\n### API\n\n1. -[X] Checked\n- TEST    : Use `/v1/address/new` method (few for TONs and few for Jettons for different users). \n            Check new addresses in DB\n- RESULT  : You must receive different addresses in user-friendly format with `bounce = false` flag and \n            testnet flag correlated with `IS_TESTNET` env var and raw in DB. For Jetton deposits it must be an\n            owner address.\n- COMMENT :\n\n2. -[X] Checked\n- TEST    : Use `/v1/address/all{?user_id}` method and compare with addresses created at 1. And check it by DB\n- RESULT  : All addresses must be received and equal to those created earlier\n- COMMENT :\n\n3. -[X] Checked\n- TEST    : Check `/v1/income{?user_id}` for new empty deposits\n- RESULT  : Income must be zero. The addresses must match the addresses obtained by method `/v1/address/all{?user_id}`.\n- COMMENT :\n\n4. -[X] Checked\n- TEST    : Make some payments at deposits and check it by `/v1/income{?user_id}` method \n            with different `DEPOSIT_SIDE_BALANCE` env var\n- RESULT  : Income must correlate with payments sum\n- COMMENT :\n\n5. -[X] Checked\n- TEST    : Make some withdrawals by `/v1/withdrawal/send` method for TONs and Jettons with amount > hot wallet balance\n            and check it by `/v1/withdrawal/status{?id}` few times. Check status of withdrawals by transaction explorer \n            (e.g. https://testnet.tonapi.io/ or https://tonapi.io/). Check withdrawal in DB.\n- RESULT  : The withdrawal must be in the `pending` state and the wallet must not send any messages.\n            There is no any correlated messages in `external withdrawals` table.\n- COMMENT :\n\n6. -[X] Checked\n- TEST    : Make some withdrawals by `/v1/withdrawal/send` method for TONs and check it by `/v1/withdrawal/status{?id}` \n            few times and try to catch all statuses: `pending`, `processing`, `processed`. Check status of withdrawals \n            by transaction explorer (e.g. https://testnet.tonapi.io/ or https://tonapi.io/). Check withdrawal in DB.\n- RESULT  : Withdrawal status and transaction hash must correlate with explorer status. In `withdrawal_requests` DB \n            table must be `processing=true`, `processed=true` and `failed=false`, `confirmed=false` in \n            `external_withdrawals` table as final state.   \n- COMMENT :\n\n7. -[X] Checked\n- TEST    : Make some withdrawals by `/v1/withdrawal/send` method for Jettons with not deployed Jetton hot wallets \n            and withdrawals by `/v1/withdrawal/status{?id}` few times. Check status of withdrawals by \n            transaction explorer (e.g. https://testnet.tonapi.io/ or https://tonapi.io/). Check withdrawal in DB.\n- RESULT  : The withdrawal must be in the `pending` state and the wallet must not send any messages.\n            There is no any correlated messages in `external withdrawals` table.\n- COMMENT :\n\n8. -[X] Checked\n- TEST    : Make some withdrawals by `/v1/withdrawal/send` method for Jettons and TONs with invalid `binary_comment` (two comments, invalid data, too long)\n- RESULT  : The withdrawal must be rejected with bad request error.\n- COMMENT :\n\n9. -[X] Checked\n- TEST    : Make some withdrawals by `/v1/withdrawal/send` method for Jettons and TONs with short and long `comment` (not fitted into one cell)\n- RESULT  : The withdrawal must be finished successfully. Comment in explorer must correlate with request.\n- COMMENT :\n\n10. -[X] Checked\n- TEST    : Make some withdrawals by `/v1/withdrawal/send` method for Jettons and TONs with short and long `binary_comment` (not fitted into one cell)\n- RESULT  : The withdrawal must be finished successfully. Payload in explorer must correlate with request.\n- COMMENT :\n\n11. -[X] Checked\n- TEST    : Make some withdrawals by `/v1/withdrawal/send` method for Jettons with deployed Jetton hot wallets \n            and check it by `/v1/withdrawal/status{?id}` few times and try to catch all statuses: \n            `pending`, `processing`, `processed`. Check status of withdrawals by transaction explorer \n            (e.g. https://testnet.tonapi.io/ or https://tonapi.io/). Check withdrawal in DB.\n- RESULT  : Withdrawal status must correlate with explorer status. In `withdrawal_requests` DB table must be\n            `processing=true`, `processed=true` and `failed=false`, `confirmed=true` in `external_withdrawals` table\n            as final state.\n- COMMENT :\n\n12. -[X] Checked\n- TEST    : Start the service after some downtime. Check sync status by `/v1/system/sync` few times. \n- RESULT  : Start status should be `\"is_synced\": false` then become `\"is_synced\": true`\n- COMMENT :\n\n13. -[X] Checked\n- TEST    : Start service with `IS_TESTNET=true` env var. Make some withdrawals by `/v1/withdrawal/send` method to\n            TESTNET and MAINNET user-friendly form addresses. \n- RESULT  : All withdrawals must be accepted\n- COMMENT :\n\n14. -[X] Checked\n- TEST    : Start service with `IS_TESTNET=false` env var. Make some withdrawals by `/v1/withdrawal/send` method to\n            TESTNET user-friendly form addresses.\n- RESULT  : All withdrawals must be rejected\n- COMMENT :\n\n15. -[X] Checked\n- TEST    : Make some withdrawals by `/v1/withdrawal/send` method to service internal addresses \n            (hot wallet, jetton hot wallet, owner, ton deposit, jetton deposit).\n- RESULT  : All withdrawals must be rejected\n- COMMENT :\n\n16. -[X] Checked\n- TEST    : Try all methods with auth using wrong token\n- RESULT  : All requests must be rejected\n- COMMENT :\n\n17. -[X] Checked\n- TEST    : Make TON and Jetton withdrawal to -1 workchain\n- RESULT  : Withdrawals must be rejected\n- COMMENT :\n\n18. -[X] Checked\n- TEST    : Make withdrawals by `/v1/withdrawal/service/ton` and `/v1/withdrawal/service/jetton` from unknown address \n            and another network (testnet addr for mainnet address and -1 workchain address)\n- RESULT  : Withdrawals must be rejected\n- COMMENT :\n\n19. -[X] Checked\n- TEST    : Make withdrawal by `/v1/withdrawal/service/ton` and `/v1/withdrawal/service/jetton` from known internal but \n            not Jetton deposit owner and not TON deposit address (hot wallet and Jetton wallet)\n- RESULT  : Withdrawal must be rejected\n- COMMENT :\n\n20. -[X] Checked\n- TEST    : Make TON withdrawal by `/v1/withdrawal/service/ton` from known Jetton deposit owner address.\n            Check for unusual transactions in the database\n- RESULT  : Withdrawal must be accepted. All TONs must be sent from Jetton deposit owner to hot wallet. There is no\n            any deposit transactions (incomes/withdrawals) in the database. In `service_withdrawal_request` DB table\n            must be `processed = true`\n- COMMENT :\n\n21. -[X] Checked\n- TEST    : Make Jetton (not deposit Jetton type) withdrawal by `/v1/withdrawal/service/jetton` from known internal\n            Jetton deposit owner address. Check for unusual transactions in the database\n- RESULT  : Withdrawal must be accepted. Jettons must be sent from Jetton wallet to hot wallet.\n            There is no any deposit transactions (incomes/withdrawals) in the database.\n            In `service_withdrawal_request` DB table must be `processed = true`\n- COMMENT : Should be zero forward TON amount in transfer message to prevent invoking notification message and \n            incorrect interpretation hot wallet incoming message\n\n22. -[X] Checked\n- TEST    : Make Jetton withdrawal by `/v1/withdrawal/service/jetton` from known internal TON deposit address. \n            Check for unusual transactions in the database\n- RESULT  : Withdrawal must be accepted. First, there must be a TON filling transaction from hot wallet to TON deposit. \n            The status in `service_withdrawal_request` DB should be changed to `filled=true`. Later, the Jettons should\n            be sent from Jetton wallet to the hot wallet and in DB table should be `processed = true` and `filled=false`.\n            There is no any deposit transactions (incomes/withdrawals) in the database. There should be audit log \n            warning about withdrawal from TON deposit to non-hot wallet. \n- COMMENT : The balance on the deposit side is calculated correctly, but the withdrawal of Jettons (and TONs) from the \n            TON deposit occurs through the Jetton wallet and is not detected by the block scanner as an internal TON \n            withdrawal. The deposit balance on the hot wallet side is not replenished.\n\n23. -[X] Checked\n- TEST    : Make Jetton (for deposit Jetton type) withdrawal by `/v1/withdrawal/service/jetton` from known internal\n            Jetton deposit owner address.\n- RESULT  : In `service_withdrawal_request` DB table should be `processed = true` and `filled=false` with zero balances.\n            Must be warning about rejected withdrawal in audit log\n- COMMENT :\n\n24. -[X] Checked\n- TEST    : Make TON withdrawal by `/v1/withdrawal/service/ton` from Jetton owner address with zero TON balance\n- RESULT  : Withdrawal must be accepted. There should be audit log info about zero balance.\n            There is no any deposit transactions (incomes/withdrawals) in the database and no messages from hot wallet.\n            In `service_withdrawal_request` DB table must be `processed = true`\n- COMMENT :\n\n25. -[X] Checked\n- TEST    : Make Jetton withdrawal by `/v1/withdrawal/service/jetton` from Jetton owner address and from TON \n            deposit address with zero Jetton balance\n- RESULT  : Withdrawal must be accepted. There should be audit log info about zero balance. There is no any deposit \n            transactions (incomes/withdrawals) in the database and no messages from hot wallet. \n            In `service_withdrawal_request` DB table must be `processed = true`\n- COMMENT :\n\n26. -[X] Checked\n- TEST    : Set some Jetton in `JETTONS` env variable. Start service to init jetton hot wallet in DB. \n            Remove Jetton from env variable and restart. Try `/v1/address/new`, `/v1/withdrawal/send` for removed Jetton.\n- RESULT  : Must be currency error for `/v1/address/new`, `/v1/withdrawal/send`.\n- COMMENT :\n\n27. -[ ] Checked\n- TEST    : Set some Jetton in `JETTONS` env variable. Start service to init jetton hot wallet in DB.\n            Remove Jetton from env variable and restart. Try `/v1/address/all`, `/v1/income` for user \n            with removed Jetton deposits.\n- RESULT  : Removed Jetton should not appear in `/v1/address/all`, `/v1/income`.\n- COMMENT : Not implemented yet\n\n28. -[X] Checked\n- TEST    : Make some payments at deposits and check it by `/v1/history{?user_id,currency,limit,offset}` method\n            with different `DEPOSIT_SIDE_BALANCE` env var\n- RESULT  : Incomes must correlate with payments and DB `external_incomes` table. The history on the deposits side\n            should always be displayed.\n- COMMENT :\n\n29. -[ ] Checked\n- TEST    : Replenish the TON deposit from the masterchain wallet and check it by\n            `/v1/deposit/history{?user_id,currency,limit,offset}` method.\n- RESULT  : The sender's address must be displayed correctly in the history.\n- COMMENT :\n\n30. -[X] Checked\n- TEST    : Replenish the TON deposit (when it in nonexist status) with a bounceable message and check it by\n            `/v1/deposit/history{?user_id,currency,limit,offset}` method. Also check logs.\n- RESULT  : The bounced payment should not be in the history. There should be no errors in the logs, only a warning\n            about a bounced message.\n- COMMENT :\n\n31. -[X] Checked\n- TEST    : Replenish the Jetton deposit with zero forward amount and check it by\n            `/v1/deposit/history{?user_id,currency,limit,offset}` method.\n- RESULT  : The sender's address must be not presented in the history.\n- COMMENT :\n\n32. -[X] Checked\n- TEST    : Replenish the TON and Jetton deposit with some amount and check it by\n            `/v1/deposit/income{?tx_hash}` method.\n- RESULT  : Incomes must correlate with payments and DB `external_incomes` table.\n- COMMENT :\n\n33. -[X] Checked\n- TEST    : Replenish the Jetton deposit with zero forward amount and check it by\n            `/v1/deposit/income{?tx_hash}` method.\n- RESULT  : The sender's address must be not presented in the response value must be nonzero.\n- COMMENT :\n\n34. -[X] Checked\n- TEST    : Try to find non-existent `tx_hash` by `/v1/deposit/income{?tx_hash}` method.\n- RESULT  : The method should return a 404 error.\n- COMMENT :\n\n35. -[X] Checked\n- TEST    : Check balance of hot wallet by `/v1/balance{?currency}` (without address parameter) method for TON and jettons.\n- RESULT  : The method should return actual balance for hot wallet and correct withdrawal amounts.\n- COMMENT :\n\n36. -[X] Checked\n- TEST    : Check balance of custom account by `/v1/balance{?currency,address}` method for TON and jettons.\n- RESULT  : The method should return actual balance for this account.\n- COMMENT :\n\n37. -[X] Checked\n- TEST    : Check balance of custom noexist account without jetton wallet by `/v1/balance{?currency,address}` method for TON and jettons.\n- RESULT  : The method should return zero balance for this account.\n- COMMENT :\n\n38. -[X] Checked\n- TEST    : Check balance of custom account by `/v1/balance{?currency,address}` with invalid parameters (unknown currency, testnet address for mainnet).\n- RESULT  : The method should return bad request error.\n- COMMENT :\n\n39. -[X] Checked\n- TEST    : Resolve address of custom account by `/v1/resolve{?domain}` with valid `dns_smc_address` record.\n- RESULT  : The method should return valid user-friendly address with bounceable flag and testnet/mainnet flag.\n- COMMENT :\n\n40. -[X] Checked\n- TEST    : Resolve address of custom account by `/v1/resolve{?domain}` with invalid domain or without `dns_smc_address` DNS record.\n- RESULT  : The method should return not found error.\n- COMMENT :\n\n41. -[X] Checked\n- TEST    : Check last block time by `/v1/system/sync`.\n- RESULT  : Last block time must correlate with DB records.\n- COMMENT :\n\n42. -[X] Checked\n- TEST    : Check balance of hot wallet by `/v1/balance{?currency}` (without address parameter) method for TON and jettons. And check amounts of processing and pending amount.\n- RESULT  : The method should return actual balance for hot wallet and correct withdrawal amounts.\n- COMMENT :\n\n### Internal logic\n\n1. -[X] Checked\n- TEST    : Replenish the deposit with TONs and Jettons so that as a result the amount on the hot wallet is greater \n            than `hot_wallet_max_balance` when cold wallet is not active and cold wallet address in non-bounceable format. \n            Check withdrawals in DB\n- RESULT  : You must find new withdrawal in `withdrawal_requests` table with `is_internal=true` and `bounceable=false`. \n            And final status must correlate with explorer. \n- COMMENT :\n\n2. -[ ] Checked\n- TEST    : Start the service while a workchain merges and splits. Check the integrity of the chain of blocks in the \n            table by comparing with the explorer.\n- RESULT  : There should be no missing blocks in the DB.\n- COMMENT :\n\n3. -[X] Checked\n- TEST    : Replenish the deposit with TONs and Jettons so that as a result the amount on the hot wallet is greater\n            than `hot_wallet_max_balance`. Try with and without `hot_wallet_residual_balance` parameter. Check withdrawals in DB\n- RESULT  : You must find new withdrawal in `withdrawal_requests` table with `is_internal=true`. And final status\n            must correlate with explorer. Withdrawal amount must correlate with hysteresis formula \n            (and `hot_wallet_residual_balance` parameter).\n- COMMENT :\n\n4. -[X] Checked\n- TEST    : Set invalid `FORWARD_TON_AMOUNT` env (negative, > max value)\n- RESULT  : Service failed with load config error.\n- COMMENT :\n\n5. -[X] Checked\n- TEST    : Set `FORWARD_TON_AMOUNT` env to 1 nanoton. And check external withdrawals and internal withdrawals.\n- RESULT  : All withdrawals must be successful and confirmed.\n- COMMENT :\n\n6. -[X] Checked\n- TEST    : Not set `FORWARD_TON_AMOUNT` env. And check external withdrawals and internal withdrawals.\n- RESULT  : All withdrawals must be successful and confirmed. Forward ton amount must be 1 nanoton.\n- COMMENT :\n\n7. -[X] Checked\n- TEST    : Set `FORWARD_TON_AMOUNT` env to 0 nanoton. And check external withdrawals and internal withdrawals.\n- RESULT  : All withdrawals must be successful and confirmed and there is no transfer notification message for external withdrawals and 1 nanoton forward amount for internal transfer.\n- COMMENT :\n\n### Deploy\n\n1. -[X] Checked\n- TEST    : Build docker images and start `payment-postgres`, `payment-processor` services using README.md instructions\n            Check availability and functionality of service.\n- RESULT  : The API must be accessible and functional\n- COMMENT :\n\n2. -[X] Checked\n- TEST    : Start optional `payment-grafana` service using README.md instructions\n            Check availability and functionality of service.\n- RESULT  : The `payments` Grafana dashboard must be accessible and show DB data\n- COMMENT :\n\n3. -[X] Checked\n- TEST    : Start `payment-processor` with `QUEUE_ENABLED=true` env var and optional `payment-rabbitmq` service \n            using README.md instructions. Make some payments to deposits. Check availability and functionality of \n            service by RabbitMQ dashboard\n- RESULT  : Must be some message activity in RabbitMQ dashboard for exchange\n- COMMENT :\n\n4. -[ ] Checked\n- TEST    : Start `payment-test` service using technical_notes.md instructions \n            with `CIRCULATION=false` env variable. Check availability and functionality of service by Grafana dashboard.\n- RESULT  : Grafana must show prometheus metrics from `payment-test` service (deposit and total balances)\n- COMMENT :\n\n5. -[ ] Checked\n- TEST    : Start `payment-test` service using technical_notes.md instructions\n            with `CIRCULATION=true` env variable. Check availability and functionality of service by Grafana dashboard.\n- RESULT  : Grafana must show prometheus metrics from `payment-test` service (deposit and total balances) and \n            payment activity\n- COMMENT :\n\n6. -[X] Checked\n- TEST    : Start `payment-processor` with `WEBHOOK_ENDPOINT=http://localhost:3333/webhook` env var.\n            Start test webserver from `cmd/testwebhook/main.go`. Make some payments to deposits. Check payments data \n            at webserver side. Add env variable `WEBHOOK_TOKEN=123` and restart `payment-processor`. Make some payments \n            to deposits. Check payments data at webserver side.  \n- RESULT  : Must be payments log activity in webserver and warning about the absence of a token when the variable \n            `WEBHOOK_TOKEN` is not set.\n- COMMENT :\n\n7. -[X] Checked\n- TEST    : Start `payment-processor` with webhooks. Make Jetton payment to deposits with zero froward amount. \n            Check payments data at webserver side.\n- RESULT  : The sender's address must be not presented.\n- COMMENT :\n\n8. -[X] Checked\n- TEST    : Up `payment-postgres` from `docker-compose.yml` with named volume. Write some data to DB.\n            Remove the container via down (without -v flag) and up it again. Check that the data is available in the DB.\n- RESULT  : The data in the database should be preserved after the container is recreated.\n- COMMENT :\n\n9. -[X] Checked\n- TEST    : Check availability of Prometheus metrics. Run the prometheus container from the `docker-compose.yml` file \n            and check the metrics via the web interface.\n- RESULT  : Error counter metrics should be available in the Prometheus web interface.\n- COMMENT :\n\n10. -[X] Checked\n- TEST    : Check migration 0.4.x-0.5.0.sql. Create version v0.4.0 DB, then make several withdrawals \n            (preferably with lost messages) that are in the status `processed` and `processing`.\n            Then stop the processor and apply the migration. Launch the new version of the processor and \n            check that the outputs were processed normally and no duplicate outputs occurred.\n- RESULT  : Withdrawals must be made correctly, there should be no duplicate withdrawals.\n- COMMENT :\n\n### Stability test\n\n1. -[X] Checked\n- TEST    : Start `payment-test` service using technical_notes.md instructions\n            with `CIRCULATION=true` env variable for long time (with enough amount of test TONs on wallet). \n            Periodically check availability and functionality of service by Grafana dashboard and docker logs.\n- RESULT  : There should be no abnormal behavior of service and errors in log\n- COMMENT : Add reconnect when timeout expires\n\n### Highload test\n\n1. -[ ] Checked\n- TEST    : Start `payment-test` service using technical_notes.md instructions\n            with `CIRCULATION=true` env variable and depositsQty = 100 x 3 types of deposits \n            (with enough amount of test TONs on wallet).\n            Periodically check availability and functionality of service by Grafana dashboard and docker logs.\n- RESULT  : There should be no abnormal behavior of service and errors in log\n- COMMENT :\n\n2. -[ ] Checked\n- TEST    : Generate a large number of deposits (1 million) and run the processor for a long time. periodically make payments on deposits.\n  Periodically check availability and functionality of service by Grafana dashboard and docker logs.\n- RESULT  : There should be no abnormal behavior of service and errors in log\n- COMMENT :"
  },
  {
    "path": "metrics/metrics.go",
    "content": "package metrics\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"sync/atomic\"\n)\n\ntype counter struct {\n\tname, description string\n\tcounter           uint64\n}\n\nfunc (c *counter) Inc() {\n\tatomic.AddUint64(&c.counter, 1)\n}\n\nfunc (c *counter) Add(n uint64) {\n\tatomic.AddUint64(&c.counter, n)\n}\n\nfunc (c *counter) Print(w io.Writer) error {\n\tif c.description != \"\" {\n\t\t_, err := fmt.Fprintf(w, \"# HELP %s %s\\n\", c.name, c.description)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t_, err := fmt.Fprintf(w, \"# TYPE %s counter\\n%s{} %s\\n\", c.name, c.name, strconv.FormatUint(atomic.LoadUint64(&c.counter), 10))\n\treturn err\n}\n\ntype printer interface {\n\tPrint(w io.Writer) error\n}\n\nvar (\n\tErrors     = &counter{name: \"errors\", description: \"number of errors since the service was launched. see logs for details\", counter: 0}\n\tWarnings   = &counter{name: \"warnings\", description: \"number of warnings since the service was launched. see logs for details\", counter: 0}\n\tInfo       = &counter{name: \"info\", description: \"number of infos since the service was launched. see logs for details\", counter: 0}\n\tAllMetrics = []printer{Errors, Warnings, Info}\n)\n"
  },
  {
    "path": "queue/queue.go",
    "content": "package queue\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\tamqp \"github.com/rabbitmq/amqp091-go\"\n)\n\ntype AmqpClient struct {\n\tconnection   *amqp.Connection\n\tchannel      *amqp.Channel\n\texchange     string\n\tenabled      bool\n\tSubscription <-chan amqp.Delivery\n}\n\n// NewAmqpClient creates new AMQP client and declare new exchange\nfunc NewAmqpClient(uri string, enabled bool, queueName string) (*AmqpClient, error) {\n\tif !enabled {\n\t\treturn &AmqpClient{enabled: false}, nil\n\t}\n\tconn, err := amqp.Dial(uri)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tch, err := conn.Channel()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient := &AmqpClient{connection: conn, channel: ch, enabled: enabled}\n\terr = client.declareExchange(queueName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn client, nil\n}\n\nfunc (c *AmqpClient) declareExchange(exchangeName string) error {\n\terr := c.channel.ExchangeDeclare(\n\t\texchangeName,\n\t\t\"fanout\",\n\t\tfalse,\n\t\tfalse,\n\t\tfalse,\n\t\tfalse,\n\t\tnil,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.exchange = exchangeName\n\treturn nil\n}\n\n// Publish publishes any payload to queue\nfunc (c *AmqpClient) Publish(payload any) error {\n\tif !c.enabled {\n\t\treturn nil\n\t}\n\tif c.exchange == \"\" {\n\t\treturn fmt.Errorf(\"exchange not init\")\n\t}\n\tbody, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = c.channel.Publish(\n\t\tc.exchange,\n\t\t\"\",\n\t\tfalse,\n\t\tfalse,\n\t\tamqp.Publishing{\n\t\t\tContentType: \"application/json\",\n\t\t\tBody:        body,\n\t\t})\n\treturn err\n}\n"
  },
  {
    "path": "release_notes.md",
    "content": "# v0.5.0 release notes\n\n1. Add `tx_hash` field, `sort_order` parameter to `/v1/deposit/history{?user_id,currency,limit,offset}` method\n2. Add `tx_hash`, `user_id`, `query_id` fields to `/v1/withdrawal/status{?id}` method response\n3. Add `failed` status to `/v1/withdrawal/status{?id}` method and withdrawal processor do not retry failed withdrawals (retry only for lost external messages)\n4. New method `/v1/deposit/income{?tx_hash}` to find income by `tx_hash`\n5. New method `/v1/balance{?currency,address}` to get account balance from blockchain (and account status for TON) and pending and processing amounts for hot wallet\n6. Push notifications (webhook or rabbitmq) after save to db (see new notification logic in `README.md` and `technical_notes.md`)\n7. Postgres docker volume permanent by default\n8. Audit log error counters with prometheus metrics\n9. Update dependencies\n10. Bugfixes\n11. Improve stability\n12. New method `/v1/resolve{?domain}` to resolve wallet DNS record\n13. Add `last_block_gen_utime` field to `/v1/system/sync` method to get unix time of the last scanned block\n14. Add `FORWARD_TON_AMOUNT` env variable to customize `forward_ton_amount` for Jetton withdrawals\n15. Binary comment support\n\n# v0.6.0 release notes\n1. Displaying the application version at startup\n\n# v0.7.0 release notes\n1. Emulator fix\n2. Total proof check available\n\n# v0.8.0 release notes\n1. Allow to set `FORWARD_TON_AMOUNT = 0` env variable to disable transfer notification message for external jetton transfer\n2. Download last blockchain config at start"
  },
  {
    "path": "technical_notes.md",
    "content": "# Technical notes\n\n- [Glossary](#Glossary)\n- [Limitations](#Limitations)\n- [API blueprint](/docs/api.apib)\n- [Wallets generation](#Wallets-generation)\n- [Healthcheck](#Healthcheck)\n- [Transfers layouts](#Transfers-layouts)\n- [Withdrawal mechanism](#Withdrawal-mechanism)\n- [Shard tracker algorithm](#Shard-tracker-algorithm)\n- [Block scanner algorithm](#Block-scanner-algorithm)\n- [Restart policy](#Restart-policy)\n- [Service withdrawals](#Service-withdrawals)\n- [Calibration parameters](#Calibration-parameters)\n- [Freezing and deleting unused accounts](#Freezing-and-deleting-unused-accounts)\n- [Highload wallet message deduplication](#Highload-wallet-message-deduplication)\n- [Audit log](#Audit-log)\n- [Sharding](#Sharding)\n- [Notification](#Notifications)\n- [Running the test util for payment processor](#Running-the-test-util-for-payment-processor)\n\n## Glossary\n* `sharding` - when the network needs to process a large number of account transactions, in order to distribute the load, \naccounts begin to be grouped into separate blocks (shard blocks). The shard block contains accounts that have the same \nbit prefix in the address.\n* `subwallets` - a wallets (addresses) derived from the same seed but with a different subwallet_id.\n* `jetton_wallet_owner` - TON wallet (Highload V2 for hot) OR special proxy contract owner of the jetton wallet smartcontract.\n* `ext_msg` - external in message in TON blockchain\n* `int_msg` - internal message in TON blockchain\n* `external wallet` - some wallet not hot and not deposit\n* `internal withdrawal` - withdrawal from deposit wallet to hot wallet\n* `external withdrawal` - withdrawal from hot wallet to external wallet\n* `internal income` - transfer from deposit wallet to hot wallet\n* `external income` - transfer from external wallet to deposit wallet\n* `external withdrawals processor` - service that performs external withdrawals\n* `internal withdrawals processor` - service that performs internal withdrawals\n* `expiration processor` - service that tracks expired and unconfirmed withdrawals\n* `block scanner` - service that extracts and decodes transactions and messages from blocks\n* `shard tracker` - utility for receiving blocks from blockchain with custom shard prefix\n\n## Limitations\n* Supports up to 256 shards only\n* Supports only 0 workchain\n* For deposit addresses used only 32 byte addr_std \n* Var address of senders saves as nil\n* Withdrawals to deposit addresses is prohibited\n* Manual withdrawals from hot wallet is prohibited **do not withdraw funds from a hot wallet bypassing the service, this will lead to a service error**\n\n### Sub-wallets (deposit) qty limitation\n- `subwallet_id` Go type: `uint32` (postgresql bigint type)\n- `max(uint32) = 4294967295`\n- `default_subwallet_id = 698983191` (for main hot wallet)\n- DB numeration starts from default `subwallet_id`\n- part of wallets in shard (for 1 byte shard prefix): 1/256\n- maximum qty of subwallets: `(max(uint32) - default_subwallet_id)/256 = 14_046_812`\n\n## Wallets generation\n1. Generates `hot_ton_wallet` (if needed) from seed phrase as Highload V2 wallet. The first byte of the address define the shard.\n2. Generates `hot_jetton_wallet` for each Jetton. (not in shard) `hot_ton_wallet` - owner for all of this jetton wallets.\n3. Generates `deposit_ton_wallet` as V3R2 from hot wallet seed phrase and subwallet_id to set the desired shard.\n4. Generates `jetton_wallet_owner` as proxy contract with `hot_ton_wallet` address as owner and subwallet_id to set the desired shard for `deposit_jetton_wallet`.\n\n## Healthcheck\n1. Checks time on liteserver after start. If service-liteserver time diff > configured value then service not starts. To avoid fail of message expiration checks.\n2. REST API provides method to check blockchain sync flag.\n\n## Transfers layouts\n\n### Replenishment of the TON deposit by payer\nStandard case. Instead of `payer_ton_wallet` there may be another smart contract.\n1. `ext_msg` -> `payer_ton_wallet`\n2. `payer_ton_wallet` -> `msg (internal)` -> `deposit_ton_wallet`\n\n### Replenishment of the Jetton deposit by payer\nStandard case. Instead of `payer_jetton_wallet_owner` there may be another smart contract.\n1. `ext_msg (payload = transfer)` -> `payer_jetton_wallet_owner`\n2. `payer_jetton_wallet_owner` -> `int_msg (value > fees, body = transfer)` -> `payer_jetton_wallet`\n3. `payer_jetton_wallet` -> `int_msg (body = internal_transfer, init)` -> `deposit_jetton_wallet`\n4. *optional* `deposit_jetton_wallet` -> `int_msg (value = excesses, body = excesses)` -> `response_destination`\n5. *optional* `deposit_jetton_wallet` -> `int_msg (body = transfer_notification)` -> `deposit_jetton_wallet_owner`\n\n### Withdraw TONs from deposit to hot wallet\n1. `ext_msg (init, mode = 128 + 32, comment = memo)` -> `deposit_ton_wallet` // init wallet, send all TONs and destroy\n2. `deposit_ton_wallet` -> `int_msg (value = all_TONs, body = memo)` -> `hot_ton_wallet`\n\n### Withdraw Jettons from deposit to hot wallet\n1. `ext_msg` -> `hot_ton_wallet`\n2. `hot_ton_wallet` -> `int_msg (init, value > fees, body = msg_with_transfer_body)` -> `deposit_jetton_wallet_owner` // fees for notify and excesses. init of proxy contract\n3. `deposit_jetton_wallet_owner` -> `int_msg (mode = 128 + 32, body = transfer, comment = memo, forward_ton_amount > fees)` -> `deposit_jetton_wallet`\n4. `deposit_jetton_wallet` -> `int_msg (jetton_value = all_jettons, forward_ton_amount > fees, body = internal_transfer, comment = memo)` -> `hot_jetton_wallet`\n5. `hot_jetton_wallet` -> `int_msg (body = transfer_notification, comment = memo)` -> `hot_ton_wallet`\n6. `hot_jetton_wallet` -> `int_msg (value = excesses, body = excesses)` -> `hot_ton_wallet`\n\n## Withdrawal mechanism\n\n### Criteria for successful withdrawal\n\n#### TON internal withdrawal\n1. Incoming int_msg with unique memo is found at hot_ton_wallet\n2. Transaction is success (message not bounced and computation phase is success)\n\n#### Jetton internal withdrawal\n1. Incoming transfer_notification int_msg with unique memo is found at hot_ton_wallet (as owner of hot_jetton_wallet) \n\n#### TON external withdrawal\n1. Outgoing int_msg is found at hot_ton_wallet. Transaction success on the receiver's side is not checked! (it depends on user request)\n\n#### Jetton external withdrawal\n1. Incoming int_msg (excesses) with unique query_id is found at hot_ton_wallet (as response destination address)\n\n### Algorithm of internal withdrawal\n1. Get transactions on deposit_wallets from blockchain and save to DB as external incomes\n2. Periodically get last (by LT) external income for deposit from DB\n3. Check for pending internal withdrawals in DB\n4. If there is no pending internal withdrawals and last success internal withdrawal LT < last external income LT then create internal withdrawal task\n5. Check balance of deposit wallet\n6. If balance > minimum value for withdrawal then save internal withdrawal task to DB with sinceLT = last external income LT, expiration time and unique memo\n7. Make withdrawal message with memo and send\n8. *Get message from blockchain (out in_msg for TON withdrawal or success transfer in_msg for Jetton) and save to DB `finish_lt` and `failed` if needed\n9. ** Mark expired (and not found in blockchain) withdrawals in DB as `failed`\n10. Get internal income (success in_msg for TON or transfer_notification in_msg for Jetton) from blockchain at hot_wallet \n11. Save internal income to DB\n\n*- There is a specific behavior for TON withdrawal. When we make a withdrawal with the 128+32 mode from deposit wallet (i.e. the destruction of the account), \nwe reset its data and seqno. However, the external message may be in node mem-pool for some time (about 10 minutes). If during this time the account balance is replenished,\nthen this message will apply again by creating a duplicate message with the same memo. On the hot wallet side, we scan all messages with a unique (memo+LT). For withdrawal in DB we save\ntotal amount and last LT for memo.\n\n**- Since the message lifetime refers to an external message to the wallet, and the success of the withdrawal of Jettons \nis checked in the transaction on the Jetton wallet, then in order to correctly check expiration, it is necessary to save \nthe time/Lt of the intermediate transaction (on hot wallet). If the entire transaction chain ends up in one block, \nthen the order in which the intermediate and final messages are saved will also be important.\n\n### Using a proxy contract to withdraw Jettons\nIf a regular wallet (V3R2, which is stored in an empty state to avoid storage fees) is used as the jetton_wallet_owner, then the withdrawal of Jettons takes place in three stages.\n1. Replenishment of a jetton_wallet_owner from hot wallet\n2. Waiting for the replenishment to arrive and the account status will be uninit\n3. Sending a Jetton transfer message\n\nTo avoid such a complex sequence of actions, a proxy contract is used as the jetton_wallet_owner. This proxy forwards Jetton transfer message from hot wallet and self destroy.\nIn order for the Jetton wallet to have a suitable address for the shard, the proxy contract applies subwallet_id.\n\n[Proxy contract source code](https://github.com/gobicycle/ton-proxy-contract)\n\n### Algorithm of external withdrawal\n1. Get batch of TON withdrawal requests from DB with different destination addresses (in raw form) \n2. Mark withdrawal requests as `processing` in DB\n3. Make ext_msg for hot_ton_wallet with withdrawals and expiration time\n4. Save withdrawals, ext_msg uuid (as part of msg hash) and expiration time to DB\n5. Send ext_msg\n6. Get transaction with ext_msg from blockchain\n7. Get out messages from transaction, save and mark correlated withdrawals as `processed` in DB\n8. If some withdrawals is presented in ext_msg and not presented in out in_msg - mark correlated withdrawals as `failed` in DB\n9. For Jetton withdrawals: get transaction with excesses in_msg with unique query_id from blockchain and mark correlated withdrawal as `confirmed` in DB\n10. Mark expired (and not found in blockchain) withdrawals in DB as `failed` and reset withdrawal requests `processing` flag\n\n## Shard tracker algorithm\n1. Get last masterchain block\n2. Get all shard blocks from masterchain block\n3. Filter shard blocks by custom shard prefix\n4. Get parent shard block for filtered shard block. If block has two parents then filter parents by shard prefix.\n5. Repeat 4. until find last known shard block\n6. Save all found shard blocks in memory\n7. Provide the following block on request by Next() method and remove from memory\n8. If there is no blocks in memory goto 1\n\n## Block scanner algorithm\n1. Get next shard block with custom shard prefix from shard tracker\n2. Get TxIDs for block\n3. Filter TxIDs by known addresses\n4. Get TXs from blockchain by TxIDs\n5. Decode TXs and messages and save to DB\n6. Goto 1.\n\n### Nuances of detecting Jetton transactions\nSince the contract code of Jetton wallets can be different, and the standard describes only the format of the transfer \nmessage (the internal transfer is not standardized), and there may be no excess and notification messages, that is, \nthe nuances of detecting Jetton transactions. Transactions are tracked on the deposit Jetton wallet. \nIf there is a transfer notification message in the transaction, then it is decoded and the decoded data is stored \nin the database. But since there may be events that change the balance of the wallet (depending on the wallet code), \nthe balance is additionally checked for the previous block and the current one, the known value is subtracted and the \nresult is written to the database in the form of replenishment with an unknown sender.\n\n## Restart policy\nThe service must be resistant to restart and long downtime. \nAll operations before being sent to the blockchain must be saved in the database with the status and expiration time.\nWithdrawal processors and expiration processor are suspended until the block scanner is synchronized. \nThe block scanner is considered synchronized when (now - last_saved_block_gen_utime) < custom preset. \n\n## Service withdrawals\nMethod for service withdrawals available. The method is used in the following cases:\n#### 1. Mistaken transfer of TONs to the address of the proxy contract (the owner of the deposit Jetton wallet) when the TONs are still there\nIf an internal withdrawal has already been made from the Jetton wallet, then the tones from the contract proxy \nshould have already returned to the hot wallet and no additional actions are needed. \nUse `/v1/withdrawal/service/ton` if the TONs are still at proxy contract. It makes direct withdrawal of all TONs from \nproxy contract to hot wallet.\n#### 2. Mistaken transfer of Jettons to the address of TON deposit\nUse `/v1/withdrawal/service/jetton`. It makes withdrawal of all Jettons from Jetton wallet (not deposit) to hot wallet.\n**! Be careful with this method.** This method withdraw all TONs from deposit to hot wallet, but balance replenish at \nhot wallet side not detect. Use this method with zero or near zero deposit TON balance.\n#### 3. Mistaken transfer of unexpected Jetton type to the address of the proxy contract\nUse `/v1/withdrawal/service/jetton`. It makes withdrawal of all Jettons from Jetton wallet (not deposit) to hot wallet.\n\n## Calibration parameters\n### TESTNET\nSingle highload message Jetton transfer to not deployed Jetton wallet (SCALE Jetton): \n- transfer message value - 0.1 TON\n- forward TON amount - 0.02 TON (for notification message)\n- excess - 0.033 TON\n- total loss = 0.1 - 0.033 = 0.067 TON\n\nSingle highload message Jetton transfer to already deployed Jetton wallet (SCALE Jetton):\n- transfer message value - 0.1 TON\n- forward TON amount - 0.02 TON (for notification message)\n- excess - 0.042 TON\n- total loss = 0.1 - 0.042 = 0.058 TON\n\n### MAINNET\n\nSingle highload message Jetton transfer to not deployed Jetton wallet (TGR Jetton):\n- transfer message value - 0.1 TON\n- forward TON amount - 0.02 TON (for notification message)\n- excess - 0.033 TON\n- total loss = 0.1 - 0.033 = 0.067 TON\n\nSingle highload message Jetton transfer to not deployed Jetton wallet (FNZ Jetton):\n- transfer message value - 0.1 TON\n- forward TON amount - 0.02 TON (for notification message)\n- excess - 0.022 TON\n- total loss = 0.1 - 0.022 = 0.078 TON\n\n## Freezing and deleting unused accounts\nIf account do not used by a long time, and its balance under 0 by storage fee, this account freezes (by the next transaction) \nand then deletes by node (by the next transaction if balance still < 0).\nIt is dangerous for Jetton wallets (hot and cold) and when account data drops Jetton balance drops too.\nRecommended to check hot and Jetton wallet balances periodically and fill it (or use special software).\n\n## Highload wallet message deduplication\nIn order to check the success of sending separate messages in a batch, we need to identify them.\nAdding a memo to a message to make it unique distorts the user's comment.\nUse control of the uniqueness of the destination address in the batch instead of adding memo.\nDestination address is a message dest address for TON transfers, and it is a destination from ton transfer message payload \nfor Jetton transfer (to avoid deduplication by the Jetton hot wallet address).\nBecause the wrapped payload is sent to the proxy contract, then the destination address is the address of the proxy\ncontract (for service and internal Jetton withdrawals).\n\n## Audit log\nThere is an audit log to detect anomalous service behavior and unusual events in the blockchain. Service errors of a \ntechnical nature fall into the ordinary log. The audit log message contains the location where the event was detected, \nthe transaction hash (if this event was detected by the block scanner) and the text of the message.\n\nThere are three levels of warnings:\n* INFO - the event is not dangerous, but unusual\n* WARNING - the event may be potentially hazardous and should be attended to\n* ERROR - the event can pose a critical threat to the operation of the service\n\n## Sharding\n### Examples of block shard_prefix from lite client\n* `0000 100000000000000000000000000000000000000000000000000000000000`\n* `0001 100000000000000000000000000000000000000000000000000000000000`\n* `0010 100000000000000000000000000000000000000000000000000000000000`\n\n### Only one shard example\n* ` 1000000000000000000000000000000000000000000000000000000000000000` OR `0x8000000000000000`\n  It is equivalent of empty bitstring.\n\n### Default SHARD\nWe use a fixed-size address prefix (8 bits). The first 8 bits of 256 bit std_address (not workchain) and workchain = 0.\nAnd all addresses will be in the same shard up to 2^8 (256) shards.\nThe default `SHARD` value is taken from the hot wallet address. Hot wallet address generates from seed phrase and default subwallet_id.\n\n#### Example:\n* `hot_ton_wallet_address = 0:60573d8db98cc369b7ce4ca1dadbfcbd17e82952938857a6cf14e1f8d77c811a` (raw form)\n* `SHARD = 01100000` (0x60)\n* `address_binary_prefix = 01100000` (for all deposit-addresses)\n\n##### Suitable block shard prefixes (for these addresses):\n* ` 1000000000000000000000000000000000000000000000000000000000000000` - 1 shard\n* `0110 100000000000000000000000000000000000000000000000000000000000`  - 16 shards\n* `01100000 10000000000000000000000000000000000000000000000000000000`  - 256 shards\n\n##### Not suitable block shard prefixes (for these addresses):\n* `0010 100000000000000000000000000000000000000000000000000000000000`  - invalid prefix\n* `01100000 0 100000000000000000000000000000000000000000000000000000`  - more than 256 shards. No guarantees that the address will be in the right shard.\n\n## Notifications\nThe system state is saved to the database in a single transaction, \nwhich allows you to restore the full state in the event of an abnormal restart and avoid data loss or duplication.\nHowever, sending notifications to channels (webhook or queue) is a separate operation that can be performed \nbefore or after saving the state to the database.\nIf notifications are sent before being saved to the database, and the service is restarted between these events, \nthen after the restart the notification will be sent again, which will lead to duplication.\nIf notifications are sent after saving to the database, and the service is restarted between these events, \nthen after the restart the notification will not be sent, which will lead to the loss of the notification.\nBecause the service stores all data about operations in the database and, if necessary, you can make clarifying queries, \nthen the second scenario was chosen. The first scenario requires the high-tier service to have notification deduplication logic.\n\n## Running the test util for payment processor\n**It is strictly recommended to run the test utility with the processor configured for the testnet.**\n\nOptionally you can start test environment for payment-processor. This utility generates deposits via API, \nsends TONs and Jettons from `payment-processor A` to deposits of `payment-processor B` and vice versa. \nThus, the utility circulates TONs and Jettons in a closed loop between \n`payment-processor A`->`payment-processor B`->`payment-processor A`. \nThe utility also allows to evaluate the loss of TONs for the circulation of funds, check the completeness of \nthe withdrawals and the presence of double withdrawals of funds.\n\n### Configurable parameters\n| ENV variable           | Description                                                                                            |\n|------------------------|--------------------------------------------------------------------------------------------------------|\n| `LITESERVER`           | same as for payment-processor A and B (must be the same for A and B)                                   |\n| `LITESERVER_KEY`       | same as for payment-processor A and B (must be the same for A and B)                                   |\n| `DB_URI`               | same as for payment-processor A                                                                        |\n| `HOST_A`               | host of payment-processor A, example `payment_processor_a:8081`                                        |\n| `HOST_B`               | host of payment-processor B, example `payment_processor_b:8081`                                        |\n| `API_TOKEN`            | same as for payment-processor A and B (must be the same for A and B)                                   |\n| `IS_TESTNET`           | same as for payment-processor A and B (must be the same for A and B)                                   |\n| `JETTONS`              | same as for payment-processor A and B (must be the same for A and B)                                   |\n| `TON_CUTOFFS`          | same as for payment-processor A and B (must be the same for A and B)                                   |\n| `HOT_WALLET_A`         | hot-wallet address for payment-processor A, example `kQCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseH3XZ_YiH9Y1ufw` |\n| `HOT_WALLET_B`         | hot-wallet address for payment-processor B, example `kQCdyiS-fIV9UVfI9Phswo4l2MA-hm8YseH3XZ_YiH9Y1ufw` |\n| `CIRCULATION`          | `true` for funds circulation in closed loop. Default: `false`.                                         |\n\nTo turn on TON and Jetton circulation set `CIRCULATION=true` ENV variable.\nIf you need only balance monitoring without TON and Jetton circulation set `CIRCULATION=false` ENV variable.\n\n1. The test util image of the utility is built from the same makefile as the payment processor\n```console\nmake -f Makefile\n```\n2. Prepare `.env` file for `payment-postgres` A and B services or fill environment variables in `docker-compose-test.yml` file.\n   Database scheme automatically init.\n```console\ndocker-compose -f docker-compose-test.yml up -d payment-postgres-a\ndocker-compose -f docker-compose-test.yml up -d payment-postgres-b\n```\n3. Prepare `.env` file for `payment-processor` A and B services or fill environment variables in `docker-compose-test.yml` file.\nSeeds for A and B must be different.\n```console\ndocker-compose -f docker-compose-test.yml up -d payment-processor-a\ndocker-compose -f docker-compose-test.yml up -d payment-processor-b\n```\n4. Start Grafana for services monitoring. Prepare `.env` file for `payment-grafana` service or\n   fill environment variables in `docker-compose-test.yml` file.\n```console\ndocker-compose -f docker-compose-test.yml up -d payment-grafana\n```\n5. Start `payment-prometheus` container\n```console\ndocker-compose -f docker-compose-test.yml up -d payment-prometheus\n```\n6. Prepare `.env` file for `payment-test` service or fill environment variables in `docker-compose-test.yml` file.\n```console\ndocker-compose -f docker-compose-test.yml up -d payment-test\n```"
  },
  {
    "path": "tests/docker-compose-tests.yml",
    "content": "version: '3'\n\nservices:\n\n  payment-postgres-a:\n    image: postgres:14\n    container_name: payment_processor_db_a\n    volumes:\n    - ./deploy/db/01_init.up.sql:/docker-entrypoint-initdb.d/init.sql\n    ports:\n      - \"5433:5432\"\n    restart: always\n    env_file:\n      - ../.env\n    environment:\n      POSTGRES_DB: \"payment_processor\"\n      POSTGRES_USER: \"pp_user\"\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n    networks:\n      - p-network\n\n  payment-postgres-b:\n    image: postgres:14\n    container_name: payment_processor_db_b\n    volumes:\n      - ./deploy/db/01_init.up.sql:/docker-entrypoint-initdb.d/init.sql\n    ports:\n      - \"5434:5432\"\n    restart: always\n    env_file:\n      - ../.env\n    environment:\n      POSTGRES_DB: \"payment_processor\"\n      POSTGRES_USER: \"pp_user\"\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n    networks:\n      - p-network\n\n  payment-processor-a:\n    image: payment-processor\n    container_name: payment_processor_a\n    ports:\n      - \"8082:8081\"\n    restart: unless-stopped\n    env_file:\n      - ../.env\n    environment:\n      DB_URI: \"postgres://pp_user:${POSTGRES_PASSWORD}@payment_processor_db_a:5432/payment_processor\"\n      API_TOKEN: ${API_TOKEN}\n      COLD_WALLET: ${COLD_WALLET}\n      JETTONS: ${JETTONS}\n      LITESERVER: ${LITESERVER}\n      LITESERVER_KEY: ${LITESERVER_KEY}\n      SEED: ${SEED}\n      TON_CUTOFFS: ${TON_CUTOFFS}\n      IS_TESTNET: ${IS_TESTNET}\n    networks:\n      - p-network\n\n  payment-processor-b:\n    image: payment-processor\n    container_name: payment_processor_b\n    ports:\n      - \"8083:8081\"\n    restart: unless-stopped\n    env_file:\n      - ../.env\n    environment:\n      DB_URI: \"postgres://pp_user:${POSTGRES_PASSWORD}@payment_processor_db_b:5432/payment_processor\"\n      API_TOKEN: ${API_TOKEN}\n      COLD_WALLET: ${COLD_WALLET}\n      JETTONS: ${JETTONS}\n      LITESERVER: ${LITESERVER}\n      LITESERVER_KEY: ${LITESERVER_KEY}\n      SEED: ${SEED}\n      TON_CUTOFFS: ${TON_CUTOFFS}\n      IS_TESTNET: ${IS_TESTNET}\n    networks:\n      - p-network\n\n  payment-grafana:\n    image: grafana/grafana:latest\n    container_name: payment_grafana\n    restart: always\n    ports:\n      - '3001:3000'\n    volumes:\n      - ./deploy/grafana/test/provisioning/datasources:/etc/grafana/provisioning/datasources\n      - ./deploy/grafana/test/provisioning/dashboards:/etc/grafana/provisioning/dashboards\n      - ./deploy/grafana/test/dashboards:/etc/dashboards\n    env_file:\n      - ../.env\n    environment:\n      GF_SECURITY_ADMIN_USER: admin\n      GF_SECURITY_ADMIN_PASSWORD: admin\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n    networks:\n      - p-network\n\n  payment-prometheus:\n    image: prom/prometheus:latest\n    container_name: payment_prometheus\n    restart: always\n    ports:\n      - '9090:9090'\n    volumes:\n      - ./deploy/prometheus/test:/etc/prometheus\n    user: \"$UID:$GID\"\n    command:\n      - '--config.file=/etc/prometheus/prometheus.yml'\n      - '--web.external-url=http://localhost:9090'\n    networks:\n      - p-network\n\n  payment-test:\n    image: payment-test\n    container_name: payment_test\n    restart: unless-stopped\n    env_file:\n      - ../.env\n    environment:\n      DB_URI: \"postgres://pp_user:${POSTGRES_PASSWORD}@payment_processor_db:5432/payment_processor\"\n      API_TOKEN: ${API_TOKEN}\n      COLD_WALLET: ${COLD_WALLET}\n      JETTONS: ${JETTONS}\n      LITESERVER: ${LITESERVER}\n      LITESERVER_KEY: ${LITESERVER_KEY}\n      SEED: ${SEED}\n      TON_CUTOFFS: ${TON_CUTOFFS}\n      IS_TESTNET: ${IS_TESTNET}\n      CIRCULATION: ${CIRCULATION}\n      HOST_A: ${HOST_A}\n      HOST_B: ${HOST_B}\n      HOT_WALLET_A: ${HOT_WALLET_A}\n      HOT_WALLET_B: ${HOT_WALLET_B}\n    networks:\n      - p-network\n\nnetworks:\n  p-network:\n    driver: bridge"
  },
  {
    "path": "threat_model.md",
    "content": "### List of possible vulnerabilities\nIn the format:\n- P: problem\n- T: threat\n- S: possible solutions\n- D: decision\n\n#### Untrusted node\n- P: unreliable data from the node lite server\n- T: the node operator can control the behavior of the service\n- S: use your trusted node or use a web3 library with proof checking (like tolibgo)\n- D: add a recommendation for launching your node in the readme\n\n#### Time out of sync\n- P: time out of sync between a node and service\n- T: incorrect expiration check and double withdrawals\n- S: use your trusted node and check time diff between node and service\n- D: add a recommendation for launching your node in the readme and service stops if time diff too big\n\n#### Blockchain out of sync\n- P: out of sync between a blockchain and service (by blocks)\n- T: the service may mark some transactions that have not yet been found as expired and make double withdrawals\n- S: check time diff between last processed block and actual time (time of last block)\n- D: service checks time diff between last the processed block and the actual time and does not make any withdrawals until service is synchronized\n\n#### Repeated withdrawal of TONs from the deposit\n- P: when a message is sent to wallet with 128+32 mode, the wallet contract is deleted, but the message itself remains in the node's mempool for some time. \nIf TONs arrive at the wallet address at this time, the message will be applied again (because seqno is being reset).\n- T: repeated withdrawal of TONs from the deposit to the hot wallet ignoring the cutoff for the minimum withdrawal and growth of internal fees\n- S: reducing the valid_until time for message\n- D: small valid_until time for message\n\n#### Bruteforce API Token through Time Attack\n- P: the api token can be bruteforced by the difference in the token verification time\n- T: an attacker can control the service through the API\n- S: isolation of the payment processor from the external network and use constant time function to check token \n- D: isolation recommendation in readme file and service uses constant time function to check token\n\n#### Forgery of messages with internal service data\n- P: an attacker can copy service messages with their data (like memo)\n- T: incorrect operation of the service or external behavior control from the blockchain\n- S: checking the addresses of the sender and recipient of the message and checking hash for some messages\n- D: checking the addresses of the sender and recipient of the message and checking hash for some messages\n\n#### Unexpected tonutils package functionality\n- P: the behavior of some functions may differ from what is expected\n- T: incorrect operation of the service or malicious behavior of the library\n- S: open source code review or trust the tonutils library\n- D: trust the tonutils library\n\n#### Modified wallet code in the tonutils package\n- P: the library may contain a modified wallet code with additional functionality\n- T: unexpected behavior of the wallet in the blockchain and control of the wallet from the outside of service\n- S: compare the wallet code with a trusted source and fix the library version or trust the tonutils library\n- D: trust the tonutils library\n\n#### Untrusted binary libs in tongo package\n- P: the behavior of some functions (TVM emulation) may differ from what is expected\n- T: incorrect operation of the service or malicious behavior of the tongo library\n- S: build binary libraries from the official TON repository or trust the tongo library\n- D: build binary libraries from the official TON repository\n\n#### Jetton wallets with unexpected behavior\n- P: custom tokens may have unusual behavior on blockchain\n- T: incorrect calculation of Jettons balances or large internal commissions of the service\n- S: use Jettons with known behavior and conforming to the standard\n- D: add a recommendations for valid Jettons to readme file\n\n#### A lot of expensive withdrawals\n- P: the service is trying to make a lot of expensive withdrawals (more than wallet balance) as a result, they displace other withdrawals from the message batch of 255 messages\n- T: the service stops processing external withdrawals\n- S: increasing the number of withdrawals requests from the database\n- D: the number of requested withdrawals requests is configured during operation\n\n#### Service withdrawals changes incoming Jetton balance\n- P: service withdrawal may be detected as negative incoming (and interprets as unknown tx)\n- T: incorrect calculation of Jettons balances\n- S: check dest address and not set \"unknown tx\" flag \n- D: check dest address and not set \"unknown tx\" flag \n\n#### DDOS and blocking requests\n- P: service can not process a lot of requests or may wait for blocked request (where mutex used)\n- T: the service stops processing new requests\n- S: all requests must be limited by the user\n- D: recommendations for requests limitations in the readme file\n\n#### SQL injections via `comment`, `user_id` and other text fields\n- P: the danger of injection through the request fields\n- T: executing an arbitrary query on the database\n- S: sanitize user input\n- D: use `go pgx` with sanitize\n\n#### Withdrawals to internal address\n- P: it is possible to make a withdrawal to an address and then generate the same address as the deposit address, \n     thereby making a withdrawal to the internal address\n- T: this can break the uniqueness of the addresses in the wallet message batch and break the checking \n     of the correspondence of incoming and outgoing messages\n- S: it is rare case, warning in technical notes, uniqueness check in withdrawal processor\n- D: warning in technical notes, uniqueness check in withdrawal processor\n\n#### Duplicate randomly generated UUIDs\n- P: DB error for not unique UUID in internal or service withdrawals\n- T: service crashes with fatal error\n- S: the probability to find a duplicate within 103 trillion version-4 UUIDs is one in a billion\n- D: the probability of error is too small, no action is required\n\n#### Freezing and deleting unused account\n- P: if account do not used by long time and its balance under 0 by storage fee, this account \n     freezes and then deletes by node\n- T: if Jetton wallet do not used by a long time it may be dropped by node, data with Jetton balance is cleaning\n- S: all Jettons (> cutoff) withdraws from deposits when service works normally. \n     It is dangerous for Jetton cold/hot wallets, that do not use for a long time.\n     Needs to check and fill balances periodically. \n- D: recommendation in technical_notes file to periodically check and fill TON balances on hot and cold wallets.\n     Recommendation to use software to do it automatically.\n\n#### Deposit side vs hot side balance shifting when service withdrawals\n- P: service withdrawal of Jettons from the TON deposit occurs through the Jetton wallet and is not detected by the \n     block scanner as an internal TON withdrawal\n- T: TON balance shift between deposit side and hot wallet side\n- S: use service withdrawal of Jettons from TON deposit wallet only with zero or near-zero TON balance\n- D: warning about this behavior in technical_notes file for method description\n\n#### Setting the value to \"expired\" without taking into account the allowable delay\n- P: it is impossible to absolutely precisely synchronize in time with the blockchain, so there is an \n     allowable time delay value. If you get into this gap, the \"expired\" may be incorrectly set.\n- T: double spending for external withdrawals or unnecessary internal withdrawals\n- S: check expiration taking into account time delay\n- D: check expiration taking into account time delay\n\n#### Repetitive failed transactions burning fees\n- P: with periodic withdrawal cycles, there may be situations where the transaction fails every time. \n     For example, when withdrawing to an uninitialized cold wallet with the bounce flag.\n- T: constant burning of a certain amount of TON on fees\n- S: additional checks to predict the success of the transaction and additional messages in the audit log\n- D: made an additional check on the state of the cold wallet and checking the bounce flag for withdrawal\n\n#### Too frequent withdrawals from a hot wallet to a cold wallet\n- P: if you set only the maximum cutoff for funds on the hot wallet, then the withdrawal to the cold wallet will occur \n     if this amount is exceeded, even if the amount of the excess is less than the amount of the withdrawal fee\n- T: there may be withdrawals of the amount of funds at which the amount of funds is unreasonably small, \n     which will lead to unnecessary burning of funds on fees\n- S: it is necessary to set some delta between the amount of triggering the withdrawal to the cold wallet and the \n     amount that will remain after the withdrawal\n- D: one more parameter has been added to the cutoffs - `hot_wallet_residual_balance`\n\n#### Message queue overflow in highload v2 hot wallet contract\n- P: When messages with a long expiration time are sent frequently, the queue inside the wallet contract can grow to large sizes. \n     At a certain queue size, transactions on the contract will begin to fail with the out of gas error, which will lead to the contract not working and burning funds on fees.\n     Some details here: [highload-wallet-v2-code.fc](https://github.com/ton-blockchain/ton/blob/cf83bd18933143da31a37c1e0d1d67f999d5f9ec/crypto/smartcont/highload-wallet-v2-code.fc)\n- T: The hot wallet contract will stop processing external messages correctly and will burn funds on fees. \n- S: reducing the valid_until time for message, combining messages into batches, increasing the interval between withdrawals\n- D: small valid_until time for message, use of batches, large interval between withdrawals"
  },
  {
    "path": "todo_list.md",
    "content": "## TODO\n- [x] Withdraw TON method\n- [x] Withdraw jetton method\n- [x] Generate new address API method\n- [x] Get addresses for user API method\n- [x] Send TON/Jetton API method\n- [x] Shard block scanner and tx parsing\n- [x] Batched send TONs method\n- [x] Batched send Jettons method\n- [x] TON transfer comment saving\n- [x] TON deposit withdrawal\n- [x] Custom withdrawal comment support\n- [x] Jetton transfer comment saving\n- [x] Jetton deposit withdrawal\n- [x] Deposit withdrawal validation\n- [x] Restart policy (repair after reconnect)\n- [x] Time sync with node\n- [x] Cold wallets support\n- [x] Shard merge/split detecting\n- [x] Graceful shutdown\n- [x] Healthcheck\n- [x] License\n- [x] Deploy (in need) hot wallet on start \n- [x] Queue/webhook notifications\n- [x] Deposits balances get method\n- [x] Deposit balance calculation flag (after deposit filling or hot-wallet filling)\n- [x] Threat model draft\n- [x] Anomalous behavior detecting and audit log\n- [x] Docs\n- [x] Refactoring\n- [x] Deploy scripts\n- [x] Unit tests\n- [x] Validate wallets code for tonutils v1.4.1\n- [x] Manual testing plan\n- [x] Service methods for API (cancellation of incorrect payments)\n- [x] Build emulator lib from sources\n- [x] Integration tests\n- [x] Hot wallets metrics\n- [x] Manual testing\n- [x] Jettons test list\n- [x] Fix timeouts\n- [x] Allow to start with empty Jetton env var\n- [x] Deposit side balances by default\n- [x] Fix \"outgoing message from internal incoming\" for bounced TON payment \n- [x] Add history method\n- [x] Rename balance to income and return owner address instead of jetton wallet (for queue too)\n- [x] Add history method to test plan\n- [x] Add filling deposit with bounce to test plan\n- [x] Update to tonutils-go 1.6.2\n- [x] Process masterchain addresses for external incomes\n- [x] Cold wallet withdrawal fix\n- [x] Add hysteresis to cold wallet withdrawal\n- [x] Add user id to notifications\n- [x] Add transaction hash to notifications\n- [x] Save tx hash to DB for incomes\n- [x] Add `failed` status for withdrawals and do not retry failed (at hot wallet) withdrawals\n- [x] Save tx hash to DB for withdrawals\n- [x] Get incoming by tx hash method\n- [x] Add asc-desc flag for get history method\n- [x] Add error counter as prometheus metrics\n- [x] Send events after saving to the database (there is a possibility of losing events instead of duplicating them)\n- [x] Use stable branch for emulator\n- [x] Get balance method\n- [x] Add meta to get withdrawals status method\n- [x] DNS resolver\n- [x] Check proofs\n- [x] Total withdrawals amount for get balance method\n- [x] Add last block time to /v1/system/sync method\n- [x] Forward ton amount customization\n- [x] Binary comment support for withdrawals\n- [x] Show app ver at start\n- [x] Download blockchain config at start\n- [x] Add reconnect to node when timeout expires\n- [ ] Get withdrawal by tx hash method\n- [ ] Add incorrect processing of some TON deposit replenishments for failed transaction to threat model\n- [ ] Duplicates of external withdrawals for DB backup problem\n- [ ] Avoid blocking withdrawals to an address if there is a very large amount in the queue for withdrawals to this address\n- [ ] Withdrawal cancellation mechanism\n- [ ] Jetton threat model\n- [ ] TNX compatibility test\n- [ ] Installation video manual\n- [ ] Node deploy\n- [ ] Performance optimization\n- [ ] Fix base64 public key format in .env file\n- [ ] Describe recovery scenarios\n- [ ] BOLT compatibility test\n- [ ] Not process removed Jettons\n- [ ] Separate .env files for services\n- [ ] Automatic migrations\n- [ ] SDK\n- [ ] Migration from blueprint to openapi\n- [ ] Refactor config and cutoff parameters\n- [ ] Get balances via states and check proof (not via get method)\n- [ ] Remove scam jettons from examples\n"
  },
  {
    "path": "webhook/webhook.go",
    "content": "package webhook\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"net/http\"\n\t\"time\"\n)\n\ntype Client struct {\n\tclient *http.Client\n\turi    string\n\ttoken  string\n}\n\n// NewWebhookClient creates new webhook client\nfunc NewWebhookClient(uri string, token string) (*Client, error) {\n\tif uri == \"\" {\n\t\treturn nil, fmt.Errorf(\"emty uri\")\n\t}\n\tif token == \"\" {\n\t\tlog.Infof(\"empty token for webhook\")\n\t}\n\treturn &Client{\n\t\tclient: &http.Client{Timeout: 10 * time.Second},\n\t\turi:    uri,\n\t\ttoken:  token,\n\t}, nil\n}\n\nfunc (s *Client) Publish(payload any) error {\n\tjsonData, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\trequest, err := http.NewRequest(\"POST\", s.uri, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn err\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json; charset=UTF-8\")\n\tif s.token != \"\" {\n\t\trequest.Header.Add(\"Authorization\", \"Bearer \"+s.token)\n\t}\n\tfor i := 0; i < 3; i++ {\n\t\terr := send(s.client, request)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"webhook sending error: %v\", err)\n\t\t\tcontinue\n\t\t}\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"attempts to send a webhook ended\")\n}\n\nfunc send(client *http.Client, request *http.Request) error {\n\tresponse, err := client.Do(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"webhook sending error: %v\", err)\n\t}\n\tdefer func() {\n\t\terr := response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"response body close error: %v\", err)\n\t\t}\n\t}()\n\tif response.StatusCode == 200 {\n\t\treturn nil\n\t} else {\n\t\treturn fmt.Errorf(\"webhook response status: %v\", response.Status)\n\t}\n}\n"
  }
]