From 6bb2289ebe18e667f4829d60c12e50ad35b4ccf2 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 10 Jul 2023 13:39:52 +0700 Subject: [PATCH 01/75] init oraidex sync for oraidex info --- packages/oraidex-sync/.env.example | 1 + packages/oraidex-sync/package.json | 16 + packages/oraidex-sync/src/db.ts | 18 ++ packages/oraidex-sync/src/helper.ts | 23 ++ packages/oraidex-sync/src/index.ts | 35 +++ packages/oraidex-sync/tsconfig.json | 15 + yarn.lock | 469 +++++++++++++++++++++++++++- 7 files changed, 567 insertions(+), 10 deletions(-) create mode 100644 packages/oraidex-sync/.env.example create mode 100644 packages/oraidex-sync/package.json create mode 100644 packages/oraidex-sync/src/db.ts create mode 100644 packages/oraidex-sync/src/helper.ts create mode 100644 packages/oraidex-sync/src/index.ts create mode 100644 packages/oraidex-sync/tsconfig.json diff --git a/packages/oraidex-sync/.env.example b/packages/oraidex-sync/.env.example new file mode 100644 index 00000000..118262d7 --- /dev/null +++ b/packages/oraidex-sync/.env.example @@ -0,0 +1 @@ +RPC_URL=https://rpc.orai.io/ \ No newline at end of file diff --git a/packages/oraidex-sync/package.json b/packages/oraidex-sync/package.json new file mode 100644 index 00000000..17cb8a8a --- /dev/null +++ b/packages/oraidex-sync/package.json @@ -0,0 +1,16 @@ +{ + "name": "@oraichain/oraidex-sync", + "version": "1.0.0", + "main": "build/index.js", + "license": "MIT", + "files": [ + "build/", + "data/" + ], + "dependencies": { + "@cosmjs/tendermint-rpc": "^0.31.0", + "@oraichain/cosmos-rpc-sync": "^1.0.5", + "@oraichain/oraidex-contracts-sdk": "^1.0.13", + "duckdb-async": "^0.8.1" + } +} diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts new file mode 100644 index 00000000..0c4fb409 --- /dev/null +++ b/packages/oraidex-sync/src/db.ts @@ -0,0 +1,18 @@ +import fs from "fs"; +import { Database, Connection } from "duckdb-async"; + +export class DuckDb { + private db: Database; + + closeDuckDb(): void { + this.db.close(); + } + + async initDuckDb(fileName?: string): Promise { + this.db = await Database.create(fileName ?? "oraidex-sync-data"); + } + + async initDuckDbConnection(): Promise { + return this.db.connect(); + } +} diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts new file mode 100644 index 00000000..82e732d9 --- /dev/null +++ b/packages/oraidex-sync/src/helper.ts @@ -0,0 +1,23 @@ +import { Attribute, Event } from "@cosmjs/stargate"; +import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; + +function parseAssetInfo(info: AssetInfo): string { + if ("native_token" in info) return info.native_token.denom; + return info.token.contract_addr; +} + +async function delay(timeout: number) { + return new Promise((resolve) => setTimeout(resolve, timeout)); +} + +function parseWasmEvents(events: readonly Event[]): (readonly Attribute[])[] { + return events + .filter((event) => event.type === "wasm") + .map((event) => event.attributes); +} + +function parseOraidexAttributes(attributes: readonly Attribute[]) { + return; +} + +export { parseAssetInfo, delay, parseWasmEvents, parseOraidexAttributes }; diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts new file mode 100644 index 00000000..17fae8ab --- /dev/null +++ b/packages/oraidex-sync/src/index.ts @@ -0,0 +1,35 @@ +import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import "dotenv/config"; +import { parseWasmEvents } from "./helper"; +import { DuckDb } from "./db"; +import { WriteData, SyncData, Txs } from "@oraichain/cosmos-rpc-sync"; +import "dotenv/config"; + +const duckDb = new DuckDb(); +class WriteOrders extends WriteData { + constructor() { + super(); + } + + async process(chunk: any): Promise { + try { + const { txs, offset: newOffset, queryTags } = chunk as Txs; + } catch (error) { + console.log("error processing data: ", error); + return false; + } + return true; + } +} + +const sync = async () => { + try { + await duckDb.initDuckDb(); + const rpcUrl = process.env.RPC_URL || "https://rpc.orai.io"; + const client = await CosmWasmClient.connect(rpcUrl); + } catch (error) { + console.log("error in start: ", error); + } +}; + +export { duckDb, sync }; diff --git a/packages/oraidex-sync/tsconfig.json b/packages/oraidex-sync/tsconfig.json new file mode 100644 index 00000000..fa0134db --- /dev/null +++ b/packages/oraidex-sync/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "build", + "declaration": true, + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules/" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1e292734..941a125b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,6 +41,16 @@ "@cosmjs/math" "^0.29.5" "@cosmjs/utils" "^0.29.5" +"@cosmjs/amino@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.31.0.tgz#49b33047295002804ad51bdf7ec0c2c97f1b553d" + integrity sha512-xJ5CCEK7H79FTpOuEmlpSzVI+ZeYESTVvO3wHDgbnceIyAne3C68SvyaKqLUR4uJB0Z4q4+DZHbqW6itUiv4lA== + dependencies: + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + "@cosmjs/cosmwasm-stargate@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.29.5.tgz#3f257da682658833e0f4eb9e8ff758e4d927663a" @@ -71,6 +81,19 @@ elliptic "^6.5.4" libsodium-wrappers "^0.7.6" +"@cosmjs/crypto@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.31.0.tgz#0be3867ada0155da19c45a51f5fde08e84f9ec4b" + integrity sha512-UaqCe6Tgh0pe1QlZ66E13t6FlIF86QrnBXXq+EN7Xe1Rouza3fJ1ojGlPleJZkBoq3tAyYVIOOqdZIxtVj/sIQ== + dependencies: + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + "@noble/hashes" "^1" + bn.js "^5.2.0" + elliptic "^6.5.4" + libsodium-wrappers-sumo "^0.7.11" + "@cosmjs/encoding@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.29.5.tgz#009a4b1c596cdfd326f30ccfa79f5e56daa264f2" @@ -80,6 +103,15 @@ bech32 "^1.1.4" readonly-date "^1.0.0" +"@cosmjs/encoding@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.31.0.tgz#9a6fd80b59c35fc20638a6436128ad0be681eafc" + integrity sha512-NYGQDRxT7MIRSlcbAezwxK0FqnaSPKCH7O32cmfpHNWorFxhy9lwmBoCvoe59Kd0HmArI4h+NGzLEfX3OLnA4Q== + dependencies: + base64-js "^1.3.0" + bech32 "^1.1.4" + readonly-date "^1.0.0" + "@cosmjs/json-rpc@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.29.5.tgz#5e483a9bd98a6270f935adf0dfd8a1e7eb777fe4" @@ -88,6 +120,14 @@ "@cosmjs/stream" "^0.29.5" xstream "^11.14.0" +"@cosmjs/json-rpc@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.31.0.tgz#38fda21188f2046db4a111fb5463ccde3c3751d7" + integrity sha512-Ix2Cil2qysiLNrX+E0w3vtwCrqxGVq8jklpLA7B2vtMrw7tru/rS65fdFSy8ep0wUNLL6Ud32VXa5K0YObDOMA== + dependencies: + "@cosmjs/stream" "^0.31.0" + xstream "^11.14.0" + "@cosmjs/math@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.29.5.tgz#722c96e080d6c2b62215ce9f4c70da7625b241b6" @@ -95,6 +135,13 @@ dependencies: bn.js "^5.2.0" +"@cosmjs/math@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.31.0.tgz#c9fc5f8191df7c2375945d2eacce327dfbf26414" + integrity sha512-Sb/8Ry/+gKJaYiV6X8q45kxXC9FoV98XCY1WXtu0JQwOi61VCG2VXsURQnVvZ/EhR/CuT/swOlNKrqEs3da0fw== + dependencies: + bn.js "^5.2.0" + "@cosmjs/proto-signing@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.29.5.tgz#af3b62a46c2c2f1d2327d678b13b7262db1fe87c" @@ -108,6 +155,19 @@ cosmjs-types "^0.5.2" long "^4.0.0" +"@cosmjs/proto-signing@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.31.0.tgz#7056963457cd967f53f56c2ab4491638e5ade2c0" + integrity sha512-JNlyOJRkn8EKB9mCthkjr6lVX6eyVQ09PFdmB4/DR874E62dFTvQ+YvyKMAgN7K7Dcjj26dVlAD3f6Xs7YOGDg== + dependencies: + "@cosmjs/amino" "^0.31.0" + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + cosmjs-types "^0.8.0" + long "^4.0.0" + "@cosmjs/socket@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.29.5.tgz#a48df6b4c45dc6a6ef8e47232725dd4aa556ac2d" @@ -118,6 +178,16 @@ ws "^7" xstream "^11.14.0" +"@cosmjs/socket@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.31.0.tgz#ffcae48251a68b4a1c37a1c996d8b123cd8ad5ac" + integrity sha512-WDh9gTyiP3OCXvSAJJn33+Ef3XqMWag+bpR1TdMBxTmlTxuvU+kPy4cf6P2OF+jkkUBEA5Se2EAju0eFbJMT+w== + dependencies: + "@cosmjs/stream" "^0.31.0" + isomorphic-ws "^4.0.1" + ws "^7" + xstream "^11.14.0" + "@cosmjs/stargate@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.29.5.tgz#d597af1c85a3c2af7b5bdbec34d5d40692cc09e4" @@ -136,6 +206,24 @@ protobufjs "~6.11.3" xstream "^11.14.0" +"@cosmjs/stargate@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.31.0.tgz#a7df1eaf1363513529607abaa52a5045aaaee0fd" + integrity sha512-GYhk9lzZPj/QmYHC0VV/4AMoRzVcOP+EnB1YZCoWlBdLuVmpBYKRagJqWIrIwdk1E0gF2ZoESd2TYfdh1fqIpg== + dependencies: + "@confio/ics23" "^0.6.8" + "@cosmjs/amino" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/proto-signing" "^0.31.0" + "@cosmjs/stream" "^0.31.0" + "@cosmjs/tendermint-rpc" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + cosmjs-types "^0.8.0" + long "^4.0.0" + protobufjs "~6.11.3" + xstream "^11.14.0" + "@cosmjs/stream@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.29.5.tgz#350981cac496d04939b92ee793b9b19f44bc1d4e" @@ -143,6 +231,13 @@ dependencies: xstream "^11.14.0" +"@cosmjs/stream@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.31.0.tgz#7faf0f5ccd5ceffdd3b5d9fb81e292bb7a930b2c" + integrity sha512-Y+aSHwhHkLGIaQOdqRob+yga2zr9ifl9gZDKD+B7+R5pdWN5f2TTDhYWxA6YZcZ6xRmfr7u8a7tDh7iYLC/zKA== + dependencies: + xstream "^11.14.0" + "@cosmjs/tendermint-rpc@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.29.5.tgz#f205c10464212bdf843f91bb2e4a093b618cb5c2" @@ -159,11 +254,32 @@ readonly-date "^1.0.0" xstream "^11.14.0" +"@cosmjs/tendermint-rpc@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.31.0.tgz#df82f634ff08fc377dfdccea43a31d92b5b0eaf1" + integrity sha512-yo9xbeuI6UoEKIhFZ9g0dvUKLqnBzwdpEc/uldQygQc51j38gQVwFko+6sjmhieJqRYYvrYumcbJMiV6GFM9aA== + dependencies: + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/json-rpc" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/socket" "^0.31.0" + "@cosmjs/stream" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + axios "^0.21.2" + readonly-date "^1.0.0" + xstream "^11.14.0" + "@cosmjs/utils@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.29.5.tgz#3fed1b3528ae8c5f1eb5d29b68755bebfd3294ee" integrity sha512-m7h+RXDUxOzEOGt4P+3OVPX7PuakZT3GBmaM/Y2u+abN3xZkziykD/NvedYFvvCCdQo714XcGl33bwifS9FZPQ== +"@cosmjs/utils@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.31.0.tgz#3a7ac16856dcff63bbf1bb11e31f975f71ef4f21" + integrity sha512-nNcycZWUYLNJlrIXgpcgVRqdl6BXjF4YlXdxobQWpW9Tikk61bEGeAFhDYtC0PwHlokCNw0KxWiHGJL4nL7Q5A== + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -181,6 +297,18 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@isaacs/string-locale-compare@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz#291c227e93fd407a96ecd59879a35809120e432b" @@ -895,6 +1023,21 @@ npmlog "^6.0.2" write-file-atomic "^4.0.1" +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c" + integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@noble/hashes@^1", "@noble/hashes@^1.0.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" @@ -969,6 +1112,13 @@ "@gar/promisify" "^1.1.3" semver "^7.3.5" +"@npmcli/fs@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-3.1.0.tgz#233d43a25a91d68c3a863ba0da6a3f00924a173e" + integrity sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w== + dependencies: + semver "^7.3.5" + "@npmcli/git@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-3.0.2.tgz#5c5de6b4d70474cf2d09af149ce42e4e1dacb931" @@ -1231,6 +1381,13 @@ dependencies: "@octokit/openapi-types" "^17.1.1" +"@oraichain/cosmos-rpc-sync@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@oraichain/cosmos-rpc-sync/-/cosmos-rpc-sync-1.0.5.tgz#252e2434c3fe1a2e82793cf42b842e9d939e3aa1" + integrity sha512-8QTlXEE2p/S2jH3tsVYBX+7zCPvNgOdfkz3CMMkNOKaTtiZc+9Q9SvuEb4ED2exhuD29MVzIMLpqjRicIHTEvA== + dependencies: + "@cosmjs/stargate" "^0.31.0" + "@parcel/watcher@2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" @@ -1239,6 +1396,11 @@ node-addon-api "^3.2.1" node-gyp-build "^4.3.0" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -1476,6 +1638,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-sequence-parser@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz#4d790f31236ac20366b23b3916b789e1bde39aed" @@ -1495,11 +1662,24 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + "aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + are-we-there-yet@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" @@ -1730,6 +1910,24 @@ cacache@^16.0.0, cacache@^16.0.6, cacache@^16.1.0: tar "^6.1.11" unique-filename "^2.0.0" +cacache@^17.0.0: + version "17.1.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.3.tgz#c6ac23bec56516a7c0c52020fd48b4909d7c7044" + integrity sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg== + dependencies: + "@npmcli/fs" "^3.1.0" + fs-minipass "^3.0.0" + glob "^10.2.2" + lru-cache "^7.7.1" + minipass "^5.0.0" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + p-map "^4.0.0" + ssri "^10.0.0" + tar "^6.1.11" + unique-filename "^3.0.0" + call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -1884,7 +2082,7 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-support@^1.1.3: +color-support@^1.1.2, color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== @@ -1940,7 +2138,7 @@ config-chain@^1.1.12: ini "^1.3.4" proto-list "~1.2.1" -console-control-strings@^1.1.0: +console-control-strings@^1.0.0, console-control-strings@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== @@ -2051,6 +2249,14 @@ cosmjs-types@^0.5.2: long "^4.0.0" protobufjs "~6.11.2" +cosmjs-types@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/cosmjs-types/-/cosmjs-types-0.8.0.tgz#2ed78f3e990f770229726f95f3ef5bf9e2b6859b" + integrity sha512-Q2Mj95Fl0PYMWEhA2LuGEIhipF7mQwd9gTQ85DdP9jjjopeoGaDxvmPa5nakNzsq7FnO1DMTatXTAx6bxMH7Lg== + dependencies: + long "^4.0.0" + protobufjs "~6.11.2" + "cosmwasm-vm-js-zk@https://github.com/oraichain/cosmwasm-vm-js-zk.git": version "0.0.0" resolved "https://github.com/oraichain/cosmwasm-vm-js-zk.git#92f31a12b096c87fc1e4ea8552e8b184ed0ff8c8" @@ -2060,7 +2266,7 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2167,6 +2373,11 @@ detect-indent@^6.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + dezalgo@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" @@ -2211,11 +2422,32 @@ dotenv@~10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== +duckdb-async@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/duckdb-async/-/duckdb-async-0.8.1.tgz#258be141def7c3a1ac594e6b894bdd9d136dd351" + integrity sha512-/g5/zxC5XXXeQVEs86AtWVnzOmS7FBgVm976sJ75Tvrovms+QC5V+0SLNOl6VJQUT55NvvlrwX1zQybQfa+48w== + dependencies: + duckdb "0.8.1" + +duckdb@0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/duckdb/-/duckdb-0.8.1.tgz#73b689701e31951e7b2cf04b954188746e7773ef" + integrity sha512-a2SJDuvBVKy5muYFxXTANlqdNX1daF3NHzpqRdrk0Qx5n3Sh7BxL66O+WY9epaDFukiXEpz45sds5T1LaPaHog== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + node-addon-api "*" + node-gyp "^9.3.0" + duplexer@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ejs@^3.1.7: version "3.1.9" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" @@ -2241,6 +2473,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + encoding@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" @@ -2324,6 +2561,11 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +exponential-backoff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" + integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== + external-editor@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" @@ -2427,6 +2669,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -2467,6 +2717,13 @@ fs-minipass@^2.0.0, fs-minipass@^2.1.0: dependencies: minipass "^3.0.0" +fs-minipass@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.2.tgz#5b383858efa8c1eb8c33b39e994f7e8555b8b3a3" + integrity sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g== + dependencies: + minipass "^5.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2477,6 +2734,21 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + gauge@^4.0.3: version "4.0.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" @@ -2603,6 +2875,17 @@ glob@7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^10.2.2: + version "10.3.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.3.tgz#8360a4ffdd6ed90df84aa8d52f21f452e86a123b" + integrity sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.0.3" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -2768,7 +3051,7 @@ hosted-git-info@^5.0.0: dependencies: lru-cache "^7.5.1" -http-cache-semantics@^4.1.0: +http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== @@ -3096,6 +3379,15 @@ isomorphic-ws@^4.0.1: resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== +jackspeak@^2.0.3: + version "2.2.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.1.tgz#655e8cf025d872c9c03d3eb63e8f0c024fef16a6" + integrity sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jake@^10.8.5: version "10.8.5" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" @@ -3242,6 +3534,18 @@ libnpmpublish@^6.0.4: semver "^7.3.7" ssri "^9.0.0" +libsodium-sumo@^0.7.11: + version "0.7.11" + resolved "https://registry.yarnpkg.com/libsodium-sumo/-/libsodium-sumo-0.7.11.tgz#ab0389e2424fca5c1dc8c4fd394906190da88a11" + integrity sha512-bY+7ph7xpk51Ez2GbE10lXAQ5sJma6NghcIDaSPbM/G9elfrjLa0COHl/7P6Wb/JizQzl5UQontOOP1z0VwbLA== + +libsodium-wrappers-sumo@^0.7.11: + version "0.7.11" + resolved "https://registry.yarnpkg.com/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.11.tgz#d96329ee3c0e7ec7f5fcf4cdde16cc3a1ae91d82" + integrity sha512-DGypHOmJbB1nZn89KIfGOAkDgfv5N6SBGC3Qvmy/On0P0WD1JQvNRS/e3UL3aFF+xC0m+MYz5M+MnRnK2HMrKQ== + dependencies: + libsodium-sumo "^0.7.11" + libsodium-wrappers@^0.7.6: version "0.7.11" resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.11.tgz#53bd20606dffcc54ea2122133c7da38218f575f7" @@ -3344,6 +3648,11 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== +"lru-cache@^9.1.1 || ^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61" + integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw== + lunr@^2.3.9: version "2.3.9" resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" @@ -3357,7 +3666,7 @@ make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0: +make-dir@^3.0.0, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -3391,6 +3700,27 @@ make-fetch-happen@^10.0.3, make-fetch-happen@^10.0.6: socks-proxy-agent "^7.0.0" ssri "^9.0.0" +make-fetch-happen@^11.0.3: + version "11.1.1" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz#85ceb98079584a9523d4bf71d32996e7e208549f" + integrity sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w== + dependencies: + agentkeepalive "^4.2.1" + cacache "^17.0.0" + http-cache-semantics "^4.1.1" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^7.7.1" + minipass "^5.0.0" + minipass-fetch "^3.0.0" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.3" + promise-retry "^2.0.1" + socks-proxy-agent "^7.0.0" + ssri "^10.0.0" + map-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" @@ -3501,6 +3831,13 @@ minimatch@^9.0.0: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -3533,6 +3870,17 @@ minipass-fetch@^2.0.3: optionalDependencies: encoding "^0.1.13" +minipass-fetch@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-3.0.3.tgz#d9df70085609864331b533c960fd4ffaa78d15ce" + integrity sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ== + dependencies: + minipass "^5.0.0" + minipass-sized "^1.0.3" + minizlib "^2.1.2" + optionalDependencies: + encoding "^0.1.13" + minipass-flush@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" @@ -3574,6 +3922,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.1" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.1.tgz#dff63464407cd8b83d7f008c0f116fa8c9b77ebf" + integrity sha512-NQ8MCKimInjVlaIqx51RKJJB7mINVkLTJbsZKmto4UAAOC/CWXES8PGaOgoBZyqoUsUA/U3DToGK7GJkkHbjJw== + minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -3637,6 +3990,11 @@ neo-async@^2.6.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +node-addon-api@*: + version "7.0.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.0.0.tgz#8136add2f510997b3b94814f4af1cce0b0e3962e" + integrity sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA== + node-addon-api@^1.7.1: version "1.7.2" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" @@ -3680,6 +4038,23 @@ node-gyp@^9.0.0: tar "^6.1.2" which "^2.0.2" +node-gyp@^9.3.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.0.tgz#2a7a91c7cba4eccfd95e949369f27c9ba704f369" + integrity sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg== + dependencies: + env-paths "^2.2.0" + exponential-backoff "^3.1.1" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^11.0.3" + nopt "^6.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + nopt@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" @@ -3814,6 +4189,16 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + npmlog@^6.0.0, npmlog@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" @@ -3875,6 +4260,11 @@ nx@15.9.4, "nx@>=14.8.1 < 16": "@nrwl/nx-win32-arm64-msvc" "15.9.4" "@nrwl/nx-win32-x64-msvc" "15.9.4" +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -4143,6 +4533,14 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -4574,11 +4972,16 @@ shiki@^0.14.1: vscode-oniguruma "^1.7.0" vscode-textmate "^8.0.0" -signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" + integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== + slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" @@ -4675,6 +5078,13 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +ssri@^10.0.0: + version "10.0.4" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.4.tgz#5a20af378be586df139ddb2dfb3bf992cf0daba6" + integrity sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ== + dependencies: + minipass "^5.0.0" + ssri@^9.0.0, ssri@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057" @@ -4682,7 +5092,7 @@ ssri@^9.0.0, ssri@^9.0.1: dependencies: minipass "^3.1.1" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4691,6 +5101,15 @@ ssri@^9.0.0, ssri@^9.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -4705,13 +5124,20 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -4970,6 +5396,13 @@ unique-filename@^2.0.0: dependencies: unique-slug "^3.0.0" +unique-filename@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" + integrity sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g== + dependencies: + unique-slug "^4.0.0" + unique-slug@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9" @@ -4977,6 +5410,13 @@ unique-slug@^3.0.0: dependencies: imurmurhash "^0.1.4" +unique-slug@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-4.0.0.tgz#6bae6bb16be91351badd24cdce741f892a6532e3" + integrity sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ== + dependencies: + imurmurhash "^0.1.4" + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" @@ -5099,7 +5539,7 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" -wide-align@^1.1.5: +wide-align@^1.1.2, wide-align@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== @@ -5111,7 +5551,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -5120,6 +5560,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" From 790f3ea935ec2e0f77f59c4460359e470cd6369f Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 11 Jul 2023 11:06:58 +0700 Subject: [PATCH 02/75] parsed txs to collect oraidex msg execute contracts --- .gitignore | 4 +- packages/oraidex-sync/package.json | 3 + packages/oraidex-sync/src/db.ts | 22 ++++++- packages/oraidex-sync/src/helper.ts | 97 ++++++++++++++++++++++++++++- packages/oraidex-sync/src/index.ts | 22 ++++++- packages/oraidex-sync/src/types.ts | 67 ++++++++++++++++++++ 6 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 packages/oraidex-sync/src/types.ts diff --git a/.gitignore b/.gitignore index 5abeb64f..57902e3c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ typings/ .env .npmrc -build \ No newline at end of file +build +*data +*data.wal \ No newline at end of file diff --git a/packages/oraidex-sync/package.json b/packages/oraidex-sync/package.json index 17cb8a8a..d2d445f7 100644 --- a/packages/oraidex-sync/package.json +++ b/packages/oraidex-sync/package.json @@ -7,6 +7,9 @@ "build/", "data/" ], + "scripts": { + "start": "npx ts-node src/index.ts" + }, "dependencies": { "@cosmjs/tendermint-rpc": "^0.31.0", "@oraichain/cosmos-rpc-sync": "^1.0.5", diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 0c4fb409..aef419d6 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -1,4 +1,3 @@ -import fs from "fs"; import { Database, Connection } from "duckdb-async"; export class DuckDb { @@ -15,4 +14,25 @@ export class DuckDb { async initDuckDbConnection(): Promise { return this.db.connect(); } + + async createHeightSnapshot() { + const db = await this.initDuckDbConnection(); + await db.all( + "CREATE TABLE IF NOT EXISTS height_snapshot (currentInd INTEGER,PRIMARY KEY (currentInd))" + ); + } + + async loadHeightSnapshot() { + const db = await this.initDuckDbConnection(); + const result = await db.all("SELECT * FROM height_snapshot"); + return result.length > 0 ? result[0] : { currentInd: 1 }; + } + + async insertHeightSnapshot(currentInd: number) { + const db = await this.initDuckDbConnection(); + await db.all( + "INSERT OR REPLACE INTO height_snapshot VALUES (?)", + currentInd + ); + } } diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index 82e732d9..83badf71 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -1,5 +1,20 @@ -import { Attribute, Event } from "@cosmjs/stargate"; -import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; +import { Attribute, Event, IndexedTx } from "@cosmjs/stargate"; +import { Tx } from "@oraichain/cosmos-rpc-sync"; +import { Asset, AssetInfo } from "@oraichain/oraidex-contracts-sdk"; +import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; +import { Tx as CosmosTx } from "cosmjs-types/cosmos/tx/v1beta1/tx"; +import { + AccountTx, + ModifiedMsgExecuteContract, + MsgExecuteContractWithLogs, + MsgType, + OraiswapPairCw20HookMsg, + ProvideLiquidityOperationData, + SwapOperationData, + TxAnlysisResult, + WithdrawLiquidityOperationData, +} from "./types"; +import { Log } from "@cosmjs/stargate/build/logs"; function parseAssetInfo(info: AssetInfo): string { if ("native_token" in info) return info.native_token.denom; @@ -20,4 +35,80 @@ function parseOraidexAttributes(attributes: readonly Attribute[]) { return; } -export { parseAssetInfo, delay, parseWasmEvents, parseOraidexAttributes }; +function parseTxLog(rawLog: string): Log[] { + return JSON.parse(rawLog); +} + +function parseTxToMsgExecuteContractMsgs(tx: Tx): MsgExecuteContractWithLogs[] { + if (tx.code !== 0) return []; + const logs: Log[] = parseTxLog(tx.rawLog); + const cosmosTx = CosmosTx.decode(tx.tx); + if (!cosmosTx.body) return []; + const msgs: MsgExecuteContractWithLogs[] = []; + for (let i = 0; i < cosmosTx.body.messages.length; i++) { + const msg = cosmosTx.body.messages[i]; + if (msg.typeUrl === "/cosmwasm.wasm.v1.MsgExecuteContract") { + const msgExecuteContract = MsgExecuteContract.decode(msg.value); + // TODO: this is an assumption that the log order is the same as the message order. + msgs.push({ ...msgExecuteContract, logs: logs[i] }); + } + } + return msgs; +} + +function parseExecuteContractToOraidexMsgs( + msgs: MsgExecuteContractWithLogs[] +): ModifiedMsgExecuteContract[] { + let objs: ModifiedMsgExecuteContract[] = []; + for (let msg of msgs) { + try { + const obj: ModifiedMsgExecuteContract = { + ...msg, + msg: JSON.parse(Buffer.from(msg.msg).toString("utf-8")), + }; + // Should be provide, remove liquidity or other oraidex related types + if ("provide_liquidity" in obj.msg || "swap" in obj.msg) objs.push(obj); + if ("send" in obj.msg) { + try { + const contractSendMsg: OraiswapPairCw20HookMsg = JSON.parse( + Buffer.from(obj.msg.send.msg, "base64").toString("utf-8") + ); + if ( + "swap" in contractSendMsg || + "withdraw_liquidity" in contractSendMsg + ) + objs.push({ ...msg, msg: contractSendMsg }); + } catch (error) { + // do nothing because we dont care about other types of msgs + } + } + } catch (error) { + // do nothing because we dont care about other types of msgs + } + } + return objs; +} + +function parseTxs(txs: Tx[]): TxAnlysisResult { + let pairAssets: Asset[][] = []; + let transactions: Tx[] = []; + let swapOpsData: SwapOperationData[] = []; + let accountTxs: AccountTx[] = []; + let provideLiquidityOpsData: ProvideLiquidityOperationData[] = []; + let withdrawLiquidityOpsData: WithdrawLiquidityOperationData[] = []; + for (let tx of txs) { + transactions.push(tx); + const msgExecuteContracts = parseTxToMsgExecuteContractMsgs(tx); + const msgs = parseExecuteContractToOraidexMsgs(msgExecuteContracts); + console.dir(msgs, { depth: null }); + } + return undefined; +} + +export { + parseAssetInfo, + delay, + parseWasmEvents, + parseOraidexAttributes, + parseTxs, +}; diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 17fae8ab..07cbcbe3 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -1,6 +1,5 @@ -import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; import "dotenv/config"; -import { parseWasmEvents } from "./helper"; +import { parseTxs, parseWasmEvents } from "./helper"; import { DuckDb } from "./db"; import { WriteData, SyncData, Txs } from "@oraichain/cosmos-rpc-sync"; import "dotenv/config"; @@ -14,6 +13,8 @@ class WriteOrders extends WriteData { async process(chunk: any): Promise { try { const { txs, offset: newOffset, queryTags } = chunk as Txs; + console.log("txs: ", txs.length); + parseTxs(txs); } catch (error) { console.log("error processing data: ", error); return false; @@ -25,11 +26,26 @@ class WriteOrders extends WriteData { const sync = async () => { try { await duckDb.initDuckDb(); + await duckDb.createHeightSnapshot(); const rpcUrl = process.env.RPC_URL || "https://rpc.orai.io"; - const client = await CosmWasmClient.connect(rpcUrl); + let { currentInd } = await duckDb.loadHeightSnapshot(); + // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic + if (currentInd <= 12388825) { + currentInd = 12388825; + } + new SyncData({ + offset: currentInd, + rpcUrl, + queryTags: [], + limit: 100, + maxThreadLevel: 1, + interval: 5000, + }).pipe(new WriteOrders()); } catch (error) { console.log("error in start: ", error); } }; +sync(); + export { duckDb, sync }; diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts new file mode 100644 index 00000000..6db76d1a --- /dev/null +++ b/packages/oraidex-sync/src/types.ts @@ -0,0 +1,67 @@ +import { Log } from "@cosmjs/stargate/build/logs"; +import { Tx } from "@oraichain/cosmos-rpc-sync"; +import { Asset, AssetInfo, Decimal } from "@oraichain/oraidex-contracts-sdk"; +import { ExecuteMsg as OraiswapPairExecuteMsg } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; +import { ExecuteMsg as OraiswapTokenMsg } from "@oraichain/oraidex-contracts-sdk/build/OraiswapToken.types"; +import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; + +export type AssetData = { + info: AssetInfo; + symbol: string; + image: string; + name: string; +}; + +export type SwapOperationData = { + txhash: string; + offerAsset: Asset; + askAsset: Asset; + taxAmount: number; + commissionAmount: number; + spreadAmount: number; +}; + +export type AccountTx = { + accountAddress: string; + txhash: string; +}; + +export type ProvideLiquidityOperationData = { + txhash: string; + firstTokenAsset: Asset; + secondTokenAsset: Asset; + provider: string; +}; + +export type TxAnlysisResult = { + pairAssets: Asset[][]; + transactions: Tx[]; + swapOpsData: SwapOperationData[]; + accountTxs: AccountTx[]; + provideLiquidityOpsData: ProvideLiquidityOperationData[]; + withdrawLiquidityOpsData: WithdrawLiquidityOperationData[]; +}; + +export type MsgExecuteContractWithLogs = MsgExecuteContract & { + logs: Log; +}; + +export type ModifiedMsgExecuteContract = Omit< + MsgExecuteContractWithLogs, + "msg" +> & { + msg: MsgType; +}; + +export type MsgType = + | OraiswapPairExecuteMsg + | OraiswapTokenMsg + | OraiswapPairCw20HookMsg; + +export type WithdrawLiquidityOperationData = ProvideLiquidityOperationData; + +export type OraiswapPairCw20HookMsg = { + swap: + | { belief_price?: Decimal; max_spread?: Decimal; to?: string } + | { withdraw_liquidity: {} }; +}; From b6af3d3f0ef47425f77c9e2237a5cb16005e656f Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 11 Jul 2023 15:09:45 +0700 Subject: [PATCH 03/75] finished parsing oraidex related events & transactions --- jest.config.js | 5 + package.json | 3 + packages/oraidex-sync/src/helper.ts | 148 +- packages/oraidex-sync/src/index.ts | 4 +- packages/oraidex-sync/src/types.ts | 48 +- packages/oraidex-sync/tests/helper.spec.ts | 9 + yarn.lock | 1537 +++++++++++++++++++- 7 files changed, 1718 insertions(+), 36 deletions(-) create mode 100644 jest.config.js create mode 100644 packages/oraidex-sync/tests/helper.spec.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..3ba9167d --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/package.json b/package.json index 1166fdf3..a16f7797 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,12 @@ }, "devDependencies": { "@terran-one/cw-simulate": "https://github.com/oraichain/cw-simulate.git", + "@types/jest": "^29.5.2", "@types/node": "^18.15.8", + "jest": "^29.5.0", "lerna": "^5.6.1", "patch-package": "^7.0.0", + "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typedoc": "^0.24.7", "typescript": "^4.8.3" diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index 83badf71..2d8b4026 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -31,10 +31,6 @@ function parseWasmEvents(events: readonly Event[]): (readonly Attribute[])[] { .map((event) => event.attributes); } -function parseOraidexAttributes(attributes: readonly Attribute[]) { - return; -} - function parseTxLog(rawLog: string): Log[] { return JSON.parse(rawLog); } @@ -56,6 +52,113 @@ function parseTxToMsgExecuteContractMsgs(tx: Tx): MsgExecuteContractWithLogs[] { return msgs; } +function extractSwapOperations( + txhash: string, + events: readonly Event[] +): SwapOperationData[] { + const wasmAttributes = parseWasmEvents(events); + let swapData: SwapOperationData[] = []; + let offerDenoms: string[] = []; + let askDenoms: string[] = []; + let commissionAmounts: string[] = []; + let offerAmounts: string[] = []; + let returnAmounts: string[] = []; + let taxAmounts: string[] = []; + let spreadAmounts: string[] = []; + for (let attrs of wasmAttributes) { + if (!attrs.find((attr) => attr.key === "swap")) continue; + for (let attr of attrs) { + if (attr.key === "offer_asset") { + offerDenoms.push(attr.value); + } + if (attr.key === "ask_asset") { + askDenoms.push(attr.value); + } + if (attr.key === "offer_amount") { + offerAmounts.push(attr.value); + } + if (attr.key === "return_amount") { + returnAmounts.push(attr.value); + } + if (attr.key === "tax_amount") { + taxAmounts.push(attr.value); + } + if (attr.key === "commission_amount") { + commissionAmounts.push(attr.value); + } + if (attr.key === "spread_amount") { + spreadAmounts.push(attr.value); + } + } + } + // TODO: check length of above data should be equal because otherwise we would miss information + for (let i = 0; i < askDenoms.length; i++) { + swapData.push({ + txhash, + offerDenom: offerDenoms[i], + offerAmount: offerAmounts[i], + returnAmount: returnAmounts[i], + commissionAmount: parseInt(commissionAmounts[i]), + spreadAmount: parseInt(spreadAmounts[i]), + taxAmount: parseInt(taxAmounts[i]), + askDenom: askDenoms[i], + }); + } + return swapData; +} + +function extractMsgProvideLiquidity( + txhash: string, + msg: MsgType, + provider: string +): ProvideLiquidityOperationData | undefined { + if ("provide_liquidity" in msg) { + return { + txhash, + firstTokenAsset: msg.provide_liquidity.assets[0], + secondTokenAsset: msg.provide_liquidity.assets[1], + provider, + }; + } + return undefined; +} + +function parseWithdrawLiquidityAssets(assets: string): string[] { + // format: "2591orai, 773ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78" + const regex = /^(\d+)([a-zA-Z\/0-9]+), (\d+)([a-zA-Z\/0-9]+)/; + const matches = assets.match(regex); + console.log("matches: ", matches); + if (!matches || matches.length < 5) return []; // check < 5 because the string should be split into two numbers and two strings + return matches.slice(1, 5); +} + +function extractMsgWithdrawLiquidity( + txhash: string, + events: readonly Event[], + withdrawer: string +): WithdrawLiquidityOperationData[] { + const withdrawData: WithdrawLiquidityOperationData[] = []; + const wasmAttributes = parseWasmEvents(events); + + for (let attrs of wasmAttributes) { + if (!attrs.find((attr) => attr.key === "withdraw_liquidity")) continue; + const assetAttr = attrs.find((attr) => attr.key === "refund_assets"); + if (!assetAttr) continue; + const assets = parseWithdrawLiquidityAssets(assetAttr.value); + // sanity check. only push data if can parse asset successfully + if (assets.length !== 4) continue; + withdrawData.push({ + txhash, + firstTokenAmount: assets[0], + firstTokenDenom: assets[1], + secondTokenAmount: assets[2], + secondTokenDenom: assets[3], + withdrawer, + }); + } + return withdrawData; +} + function parseExecuteContractToOraidexMsgs( msgs: MsgExecuteContractWithLogs[] ): ModifiedMsgExecuteContract[] { @@ -66,8 +169,13 @@ function parseExecuteContractToOraidexMsgs( ...msg, msg: JSON.parse(Buffer.from(msg.msg).toString("utf-8")), }; - // Should be provide, remove liquidity or other oraidex related types - if ("provide_liquidity" in obj.msg || "swap" in obj.msg) objs.push(obj); + // Should be provide, remove liquidity, swap, or other oraidex related types + if ( + "provide_liquidity" in obj.msg || + "execute_swap_operations" in obj.msg || + "execute_swap_operation" in obj.msg + ) + objs.push(obj); if ("send" in obj.msg) { try { const contractSendMsg: OraiswapPairCw20HookMsg = JSON.parse( @@ -90,7 +198,6 @@ function parseExecuteContractToOraidexMsgs( } function parseTxs(txs: Tx[]): TxAnlysisResult { - let pairAssets: Asset[][] = []; let transactions: Tx[] = []; let swapOpsData: SwapOperationData[] = []; let accountTxs: AccountTx[] = []; @@ -100,15 +207,36 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { transactions.push(tx); const msgExecuteContracts = parseTxToMsgExecuteContractMsgs(tx); const msgs = parseExecuteContractToOraidexMsgs(msgExecuteContracts); - console.dir(msgs, { depth: null }); + const txhash = tx.hash; + for (let msg of msgs) { + const sender = msg.sender; + swapOpsData.push(...extractSwapOperations(txhash, msg.logs.events)); + const provideLiquidityData = extractMsgProvideLiquidity( + txhash, + msg.msg, + sender + ); + if (provideLiquidityData) + provideLiquidityOpsData.push(provideLiquidityData); + withdrawLiquidityOpsData.push( + ...extractMsgWithdrawLiquidity(txhash, msg.logs.events, sender) + ); + accountTxs.push({ txhash, accountAddress: sender }); + } } - return undefined; + return { + // transactions: txs, + swapOpsData, + accountTxs, + provideLiquidityOpsData, + withdrawLiquidityOpsData, + }; } export { parseAssetInfo, delay, parseWasmEvents, - parseOraidexAttributes, parseTxs, + parseWithdrawLiquidityAssets, }; diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 07cbcbe3..6871bc99 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -13,8 +13,8 @@ class WriteOrders extends WriteData { async process(chunk: any): Promise { try { const { txs, offset: newOffset, queryTags } = chunk as Txs; - console.log("txs: ", txs.length); - parseTxs(txs); + const result = parseTxs(txs); + console.dir(result, { depth: null }); } catch (error) { console.log("error processing data: ", error); return false; diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 6db76d1a..e118a51d 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -1,7 +1,14 @@ import { Log } from "@cosmjs/stargate/build/logs"; import { Tx } from "@oraichain/cosmos-rpc-sync"; -import { Asset, AssetInfo, Decimal } from "@oraichain/oraidex-contracts-sdk"; -import { ExecuteMsg as OraiswapPairExecuteMsg } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; +import { + Addr, + Asset, + AssetInfo, + Binary, + Decimal, + Uint128, +} from "@oraichain/oraidex-contracts-sdk"; +import { ExecuteMsg as OraiswapRouterExecuteMsg } from "@oraichain/oraidex-contracts-sdk/build/OraiswapRouter.types"; import { ExecuteMsg as OraiswapTokenMsg } from "@oraichain/oraidex-contracts-sdk/build/OraiswapToken.types"; import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; @@ -14,8 +21,10 @@ export type AssetData = { export type SwapOperationData = { txhash: string; - offerAsset: Asset; - askAsset: Asset; + offerDenom: string; + offerAmount: string; + askDenom: string; + returnAmount: string; taxAmount: number; commissionAmount: number; spreadAmount: number; @@ -33,9 +42,17 @@ export type ProvideLiquidityOperationData = { provider: string; }; +export type WithdrawLiquidityOperationData = { + txhash: string; + firstTokenDenom: string; + firstTokenAmount: string; + secondTokenDenom: string; + secondTokenAmount: string; + withdrawer: string; +}; + export type TxAnlysisResult = { - pairAssets: Asset[][]; - transactions: Tx[]; + // transactions: Tx[]; swapOpsData: SwapOperationData[]; accountTxs: AccountTx[]; provideLiquidityOpsData: ProvideLiquidityOperationData[]; @@ -54,12 +71,23 @@ export type ModifiedMsgExecuteContract = Omit< }; export type MsgType = - | OraiswapPairExecuteMsg - | OraiswapTokenMsg + | { + provide_liquidity: { + assets: [Asset, Asset]; + receiver?: Addr | null; + slippage_tolerance?: Decimal | null; + }; + } + | OraiswapRouterExecuteMsg + | { + send: { + amount: Uint128; + contract: string; + msg: Binary; + }; + } | OraiswapPairCw20HookMsg; -export type WithdrawLiquidityOperationData = ProvideLiquidityOperationData; - export type OraiswapPairCw20HookMsg = { swap: | { belief_price?: Decimal; max_spread?: Decimal; to?: string } diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts new file mode 100644 index 00000000..9ba59a61 --- /dev/null +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -0,0 +1,9 @@ +import * as helper from "../src/helper"; + +describe("test-helper", () => { + it("test-parseWithdrawLiquidityAssets", () => { + const result = helper.parseWithdrawLiquidityAssets( + "2591orai, 773ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78" + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index 941a125b..77099aee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,14 @@ # yarn lockfile v1 +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + "@babel/code-frame@^7.0.0": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" @@ -9,11 +17,149 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" + integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== + dependencies: + "@babel/highlight" "^7.22.5" + +"@babel/compat-data@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.6.tgz#15606a20341de59ba02cd2fcc5086fcbe73bf544" + integrity sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3": + version "7.22.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.8.tgz#386470abe884302db9c82e8e5e87be9e46c86785" + integrity sha512-75+KxFB4CZqYRXjx4NlR4J7yGvKumBuZTmV4NV6v09dVXXkuYVYLT68N6HCzLvfJ+fWCxQsntNzKwwIXL4bHnw== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.7" + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helpers" "^7.22.6" + "@babel/parser" "^7.22.7" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.8" + "@babel/types" "^7.22.5" + "@nicolo-ribaudo/semver-v6" "^6.3.3" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + +"@babel/generator@^7.22.7", "@babel/generator@^7.7.2": + version "7.22.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.7.tgz#a6b8152d5a621893f2c9dacf9a4e286d520633d5" + integrity sha512-p+jPjMG+SI8yvIaxGgeW24u7q9+5+TGpZh8/CuB7RhBKd7RCy8FayNEFNNKrNK/eUcY/4ExQqLmyrvBXKsIcwQ== + dependencies: + "@babel/types" "^7.22.5" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz#e30d61abe9480aa5a83232eb31c111be922d2e52" + integrity sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-validator-option" "^7.22.5" + "@nicolo-ribaudo/semver-v6" "^6.3.3" + browserslist "^4.21.9" + lru-cache "^5.1.1" + +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" + integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== + +"@babel/helper-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" + integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" + integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-transforms@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz#0f65daa0716961b6e96b164034e737f60a80d2ef" + integrity sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.5", "@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + "@babel/helper-validator-identifier@^7.18.6": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== + +"@babel/helper-validator-option@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" + integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== + +"@babel/helpers@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.6.tgz#8e61d3395a4f0c5a8060f309fb008200969b5ecd" + integrity sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA== + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.6" + "@babel/types" "^7.22.5" + "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -23,6 +169,157 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" + integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.5", "@babel/parser@^7.22.7": + version "7.22.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae" + integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" + integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz#aac8d383b062c5072c647a31ef990c1d0af90272" + integrity sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/template@^7.22.5", "@babel/template@^7.3.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" + integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8": + version "7.22.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.8.tgz#4d4451d31bc34efeae01eac222b514a77aa4000e" + integrity sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.7" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.22.7" + "@babel/types" "^7.22.5" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.3.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe" + integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + "@confio/ics23@^0.6.8": version "0.6.8" resolved "https://registry.yarnpkg.com/@confio/ics23/-/ics23-0.6.8.tgz#2a6b4f1f2b7b20a35d9a0745bb5a446e72930b3d" @@ -314,11 +611,243 @@ resolved "https://registry.yarnpkg.com/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz#291c227e93fd407a96ecd59879a35809120e432b" integrity sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ== +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.6.1.tgz#b48ba7b9c34b51483e6d590f46e5837f1ab5f639" + integrity sha512-Aj772AYgwTSr5w8qnyoJ0eDYvN6bMsH3ORH1ivMotrInHLKdUz6BDlaEXHdM6kODaBIkNIyQGzsMvRdOv7VG7Q== + dependencies: + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.6.1" + jest-util "^29.6.1" + slash "^3.0.0" + +"@jest/core@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.6.1.tgz#fac0d9ddf320490c93356ba201451825231e95f6" + integrity sha512-CcowHypRSm5oYQ1obz1wfvkjZZ2qoQlrKKvlfPwh5jUXVU12TWr2qMeH8chLMuTFzHh5a1g2yaqlqDICbr+ukQ== + dependencies: + "@jest/console" "^29.6.1" + "@jest/reporters" "^29.6.1" + "@jest/test-result" "^29.6.1" + "@jest/transform" "^29.6.1" + "@jest/types" "^29.6.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.5.0" + jest-config "^29.6.1" + jest-haste-map "^29.6.1" + jest-message-util "^29.6.1" + jest-regex-util "^29.4.3" + jest-resolve "^29.6.1" + jest-resolve-dependencies "^29.6.1" + jest-runner "^29.6.1" + jest-runtime "^29.6.1" + jest-snapshot "^29.6.1" + jest-util "^29.6.1" + jest-validate "^29.6.1" + jest-watcher "^29.6.1" + micromatch "^4.0.4" + pretty-format "^29.6.1" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.6.1.tgz#ee358fff2f68168394b4a50f18c68278a21fe82f" + integrity sha512-RMMXx4ws+Gbvw3DfLSuo2cfQlK7IwGbpuEWXCqyYDcqYTI+9Ju3a5hDnXaxjNsa6uKh9PQF2v+qg+RLe63tz5A== + dependencies: + "@jest/fake-timers" "^29.6.1" + "@jest/types" "^29.6.1" + "@types/node" "*" + jest-mock "^29.6.1" + +"@jest/expect-utils@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.1.tgz#ab83b27a15cdd203fe5f68230ea22767d5c3acc5" + integrity sha512-o319vIf5pEMx0LmzSxxkYYxo4wrRLKHq9dP1yJU7FoPTB0LfAKSz8SWD6D/6U3v/O52t9cF5t+MeJiRsfk7zMw== + dependencies: + jest-get-type "^29.4.3" + +"@jest/expect@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.6.1.tgz#fef18265188f6a97601f1ea0a2912d81a85b4657" + integrity sha512-N5xlPrAYaRNyFgVf2s9Uyyvr795jnB6rObuPx4QFvNJz8aAjpZUDfO4bh5G/xuplMID8PrnuF1+SfSyDxhsgYg== + dependencies: + expect "^29.6.1" + jest-snapshot "^29.6.1" + +"@jest/fake-timers@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.6.1.tgz#c773efddbc61e1d2efcccac008139f621de57c69" + integrity sha512-RdgHgbXyosCDMVYmj7lLpUwXA4c69vcNzhrt69dJJdf8azUrpRh3ckFCaTPNjsEeRi27Cig0oKDGxy5j7hOgHg== + dependencies: + "@jest/types" "^29.6.1" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.6.1" + jest-mock "^29.6.1" + jest-util "^29.6.1" + +"@jest/globals@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.6.1.tgz#c8a8923e05efd757308082cc22893d82b8aa138f" + integrity sha512-2VjpaGy78JY9n9370H8zGRCFbYVWwjY6RdDMhoJHa1sYfwe6XM/azGN0SjY8kk7BOZApIejQ1BFPyH7FPG0w3A== + dependencies: + "@jest/environment" "^29.6.1" + "@jest/expect" "^29.6.1" + "@jest/types" "^29.6.1" + jest-mock "^29.6.1" + +"@jest/reporters@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.6.1.tgz#3325a89c9ead3cf97ad93df3a427549d16179863" + integrity sha512-9zuaI9QKr9JnoZtFQlw4GREQbxgmNYXU6QuWtmuODvk5nvPUeBYapVR/VYMyi2WSx3jXTLJTJji8rN6+Cm4+FA== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.6.1" + "@jest/test-result" "^29.6.1" + "@jest/transform" "^29.6.1" + "@jest/types" "^29.6.1" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.6.1" + jest-util "^29.6.1" + jest-worker "^29.6.1" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.6.0": + version "29.6.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.0.tgz#0f4cb2c8e3dca80c135507ba5635a4fd755b0040" + integrity sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.0": + version "29.6.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.0.tgz#bd34a05b5737cb1a99d43e1957020ac8e5b9ddb1" + integrity sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.6.1.tgz#850e565a3f58ee8ca6ec424db00cb0f2d83c36ba" + integrity sha512-Ynr13ZRcpX6INak0TPUukU8GWRfm/vAytE3JbJNGAvINySWYdfE7dGZMbk36oVuK4CigpbhMn8eg1dixZ7ZJOw== + dependencies: + "@jest/console" "^29.6.1" + "@jest/types" "^29.6.1" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.6.1.tgz#e3e582ee074dd24ea9687d7d1aaf05ee3a9b068e" + integrity sha512-oBkC36PCDf/wb6dWeQIhaviU0l5u6VCsXa119yqdUosYAt7/FbQU2M2UoziO3igj/HBDEgp57ONQ3fm0v9uyyg== + dependencies: + "@jest/test-result" "^29.6.1" + graceful-fs "^4.2.9" + jest-haste-map "^29.6.1" + slash "^3.0.0" + +"@jest/transform@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.6.1.tgz#acb5606019a197cb99beda3c05404b851f441c92" + integrity sha512-URnTneIU3ZjRSaf906cvf6Hpox3hIeJXRnz3VDSw5/X93gR8ycdfSIEy19FlVx8NFmpN7fe3Gb1xF+NjXaQLWg== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.1" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.6.1" + jest-regex-util "^29.4.3" + jest-util "^29.6.1" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.1.tgz#ae79080278acff0a6af5eb49d063385aaa897bf2" + integrity sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw== + dependencies: + "@jest/schemas" "^29.6.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + "@jridgewell/resolve-uri@^3.0.3": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" @@ -332,6 +861,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + "@kiruse/serde@^0.8.0-rc.6": version "0.8.0-rc.6" resolved "https://registry.yarnpkg.com/@kiruse/serde/-/serde-0.8.0-rc.6.tgz#7de5aa930527ea2574eb47f154c40b03a2ffd12c" @@ -1038,6 +1575,11 @@ semver "^7.3.5" tar "^6.1.11" +"@nicolo-ribaudo/semver-v6@^6.3.3": + version "6.3.3" + resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz#ea6d23ade78a325f7a52750aab1526b02b628c29" + integrity sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg== + "@noble/hashes@^1", "@noble/hashes@^1.0.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" @@ -1454,6 +1996,25 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sinonjs/commons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + "@terran-one/cosmwasm-vm-js@https://github.com/oraichain/cosmwasm-vm-js.git": version "0.2.16" resolved "https://github.com/oraichain/cosmwasm-vm-js.git#7436f21f0457ee8e457e2a600b954ba508c5da1a" @@ -1514,6 +2075,73 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@types/babel__core@^7.1.14": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.1.tgz#916ecea274b0c776fec721e333e55762d3a9614b" + integrity sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.1.tgz#dd6f1d2411ae677dcb2db008c962598be31d6acf" + integrity sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg== + dependencies: + "@babel/types" "^7.20.7" + +"@types/graceful-fs@^4.1.3": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" + integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^29.5.2": + version "29.5.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.3.tgz#7a35dc0044ffb8b56325c6802a4781a626b05777" + integrity sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -1529,6 +2157,11 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/node@*": + version "20.4.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.1.tgz#a6033a8718653c50ac4962977e14d0f984d9527d" + integrity sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg== + "@types/node@>=13.7.0": version "20.1.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.1.tgz#afc492e8dbe7f672dd3a13674823522b467a45ad" @@ -1549,6 +2182,28 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/prettier@^2.1.5": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" + integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA== + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^17.0.8": + version "17.0.24" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" + integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== + dependencies: + "@types/yargs-parser" "*" + "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -1662,11 +2317,24 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + ansi-styles@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== +anymatch@^3.0.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + "aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" @@ -1771,6 +2439,66 @@ axios@^1.0.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +babel-jest@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.6.1.tgz#a7141ad1ed5ec50238f3cd36127636823111233a" + integrity sha512-qu+3bdPEQC6KZSPz+4Fyjbga5OODNcp49j6GKzG1EKbkfyJBxEYGVUmVGpwCSeGouG52R4EgYMLb6p9YeEEQ4A== + dependencies: + "@jest/transform" "^29.6.1" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.5.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz#a97db437936f441ec196990c9738d4b88538618a" + integrity sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz#57bc8cc88097af7ff6a5ab59d1cd29d52a5916e2" + integrity sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg== + dependencies: + babel-plugin-jest-hoist "^29.5.0" + babel-preset-current-node-syntax "^1.0.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -1856,6 +2584,30 @@ brorand@^1.1.0: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== +browserslist@^4.21.9: + version "4.21.9" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.9.tgz#e11bdd3c313d7e2a9e87e8b4b0c7872b13897635" + integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg== + dependencies: + caniuse-lite "^1.0.30001503" + electron-to-chromium "^1.4.431" + node-releases "^2.0.12" + update-browserslist-db "^1.0.11" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1955,6 +2707,16 @@ camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001503: + version "1.0.30001515" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz#418aefeed9d024cd3129bfae0ccc782d4cb8f12b" + integrity sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA== + chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -1964,7 +2726,7 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1972,6 +2734,11 @@ chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -1987,11 +2754,16 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -ci-info@^3.7.0: +ci-info@^3.2.0, ci-info@^3.7.0: version "3.8.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== +cjs-module-lexer@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" + integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -2058,6 +2830,16 @@ cmd-shim@^5.0.0: dependencies: mkdirp-infer-owner "^2.0.0" +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -2225,6 +3007,16 @@ conventional-recommended-bump@^6.1.0: meow "^8.0.0" q "^1.5.1" +convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -2293,7 +3085,7 @@ deasync@^0.1.15: bindings "^1.5.0" node-addon-api "^1.7.1" -debug@4, debug@^4.1.0, debug@^4.3.3: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2323,6 +3115,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + defaults@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" @@ -2378,6 +3175,11 @@ detect-libc@^2.0.0: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + dezalgo@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" @@ -2386,6 +3188,11 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +diff-sequences@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" + integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -2455,6 +3262,11 @@ ejs@^3.1.7: dependencies: jake "^10.8.5" +electron-to-chromium@^1.4.431: + version "1.4.455" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.455.tgz#81fe4353ac970eb971c07088c8da8b7f6280ddc9" + integrity sha512-8tgdX0Odl24LtmLwxotpJCVjIndN559AvaOtd67u+2mo+IDsgsTF580NB+uuDCqsHw8yFg53l5+imFV9Fw3cbA== + elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -2468,6 +3280,11 @@ elliptic@^6.5.4: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -2531,6 +3348,11 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -2561,6 +3383,23 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0, expect@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.1.tgz#64dd1c8f75e2c0b209418f2b8d36a07921adfdf1" + integrity sha512-XEdDLonERCU1n9uR56/Stx9OqojaLAQtZf9PrCHH9Hl8YXiEIka3H4NXJ3NOIBmQJTg7+j7buh34PMHfJujc8g== + dependencies: + "@jest/expect-utils" "^29.6.1" + "@types/node" "*" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.6.1" + jest-message-util "^29.6.1" + jest-util "^29.6.1" + exponential-backoff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" @@ -2597,6 +3436,11 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + fastq@^1.6.0: version "1.15.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" @@ -2604,6 +3448,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + figures@3.2.0, figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -2729,6 +3580,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -2763,6 +3619,11 @@ gauge@^4.0.3: strip-ansi "^6.0.1" wide-align "^1.1.5" +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -2787,6 +3648,11 @@ get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.3" +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-pkg-repo@^4.0.0: version "4.2.1" resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz#75973e1c8050c73f48190c52047c4cee3acbf385" @@ -2909,6 +3775,11 @@ glob@^8.0.1: minimatch "^5.0.1" once "^1.3.0" +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + globalthis@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" @@ -2935,7 +3806,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3051,6 +3922,11 @@ hosted-git-info@^5.0.0: dependencies: lru-cache "^7.5.1" +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" @@ -3256,6 +4132,11 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + is-generator-function@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" @@ -3379,6 +4260,48 @@ isomorphic-ws@^4.0.1: resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.5" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" + integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + jackspeak@^2.0.3: version "2.2.1" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.1.tgz#655e8cf025d872c9c03d3eb63e8f0c024fef16a6" @@ -3398,6 +4321,365 @@ jake@^10.8.5: filelist "^1.0.1" minimatch "^3.0.4" +jest-changed-files@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" + integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag== + dependencies: + execa "^5.0.0" + p-limit "^3.1.0" + +jest-circus@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.6.1.tgz#861dab37e71a89907d1c0fabc54a0019738ed824" + integrity sha512-tPbYLEiBU4MYAL2XoZme/bgfUeotpDBd81lgHLCbDZZFaGmECk0b+/xejPFtmiBP87GgP/y4jplcRpbH+fgCzQ== + dependencies: + "@jest/environment" "^29.6.1" + "@jest/expect" "^29.6.1" + "@jest/test-result" "^29.6.1" + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + is-generator-fn "^2.0.0" + jest-each "^29.6.1" + jest-matcher-utils "^29.6.1" + jest-message-util "^29.6.1" + jest-runtime "^29.6.1" + jest-snapshot "^29.6.1" + jest-util "^29.6.1" + p-limit "^3.1.0" + pretty-format "^29.6.1" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.6.1.tgz#99d9afa7449538221c71f358f0fdd3e9c6e89f72" + integrity sha512-607dSgTA4ODIN6go9w6xY3EYkyPFGicx51a69H7yfvt7lN53xNswEVLovq+E77VsTRi5fWprLH0yl4DJgE8Ing== + dependencies: + "@jest/core" "^29.6.1" + "@jest/test-result" "^29.6.1" + "@jest/types" "^29.6.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^29.6.1" + jest-util "^29.6.1" + jest-validate "^29.6.1" + prompts "^2.0.1" + yargs "^17.3.1" + +jest-config@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.6.1.tgz#d785344509065d53a238224c6cdc0ed8e2f2f0dd" + integrity sha512-XdjYV2fy2xYixUiV2Wc54t3Z4oxYPAELUzWnV6+mcbq0rh742X2p52pii5A3oeRzYjLnQxCsZmp0qpI6klE2cQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.6.1" + "@jest/types" "^29.6.1" + babel-jest "^29.6.1" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.6.1" + jest-environment-node "^29.6.1" + jest-get-type "^29.4.3" + jest-regex-util "^29.4.3" + jest-resolve "^29.6.1" + jest-runner "^29.6.1" + jest-util "^29.6.1" + jest-validate "^29.6.1" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.6.1" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.1.tgz#13df6db0a89ee6ad93c747c75c85c70ba941e545" + integrity sha512-FsNCvinvl8oVxpNLttNQX7FAq7vR+gMDGj90tiP7siWw1UdakWUGqrylpsYrpvj908IYckm5Y0Q7azNAozU1Kg== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.4.3" + jest-get-type "^29.4.3" + pretty-format "^29.6.1" + +jest-docblock@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" + integrity sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.6.1.tgz#975058e5b8f55c6780beab8b6ab214921815c89c" + integrity sha512-n5eoj5eiTHpKQCAVcNTT7DRqeUmJ01hsAL0Q1SMiBHcBcvTKDELixQOGMCpqhbIuTcfC4kMfSnpmDqRgRJcLNQ== + dependencies: + "@jest/types" "^29.6.1" + chalk "^4.0.0" + jest-get-type "^29.4.3" + jest-util "^29.6.1" + pretty-format "^29.6.1" + +jest-environment-node@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.6.1.tgz#08a122dece39e58bc388da815a2166c58b4abec6" + integrity sha512-ZNIfAiE+foBog24W+2caIldl4Irh8Lx1PUhg/GZ0odM1d/h2qORAsejiFc7zb+SEmYPn1yDZzEDSU5PmDkmVLQ== + dependencies: + "@jest/environment" "^29.6.1" + "@jest/fake-timers" "^29.6.1" + "@jest/types" "^29.6.1" + "@types/node" "*" + jest-mock "^29.6.1" + jest-util "^29.6.1" + +jest-get-type@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" + integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== + +jest-haste-map@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.6.1.tgz#62655c7a1c1b349a3206441330fb2dbdb4b63803" + integrity sha512-0m7f9PZXxOCk1gRACiVgX85knUKPKLPg4oRCjLoqIm9brTHXaorMA0JpmtmVkQiT8nmXyIVoZd/nnH1cfC33ig== + dependencies: + "@jest/types" "^29.6.1" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.4.3" + jest-util "^29.6.1" + jest-worker "^29.6.1" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.6.1.tgz#66a902c81318e66e694df7d096a95466cb962f8e" + integrity sha512-OrxMNyZirpOEwkF3UHnIkAiZbtkBWiye+hhBweCHkVbCgyEy71Mwbb5zgeTNYWJBi1qgDVfPC1IwO9dVEeTLwQ== + dependencies: + jest-get-type "^29.4.3" + pretty-format "^29.6.1" + +jest-matcher-utils@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.1.tgz#6c60075d84655d6300c5d5128f46531848160b53" + integrity sha512-SLaztw9d2mfQQKHmJXKM0HCbl2PPVld/t9Xa6P9sgiExijviSp7TnZZpw2Fpt+OI3nwUO/slJbOfzfUMKKC5QA== + dependencies: + chalk "^4.0.0" + jest-diff "^29.6.1" + jest-get-type "^29.4.3" + pretty-format "^29.6.1" + +jest-message-util@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.1.tgz#d0b21d87f117e1b9e165e24f245befd2ff34ff8d" + integrity sha512-KoAW2zAmNSd3Gk88uJ56qXUWbFk787QKmjjJVOjtGFmmGSZgDBrlIL4AfQw1xyMYPNVD7dNInfIbur9B2rd/wQ== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.6.1" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.6.1.tgz#049ee26aea8cbf54c764af649070910607316517" + integrity sha512-brovyV9HBkjXAEdRooaTQK42n8usKoSRR3gihzUpYeV/vwqgSoNfrksO7UfSACnPmxasO/8TmHM3w9Hp3G1dgw== + dependencies: + "@jest/types" "^29.6.1" + "@types/node" "*" + jest-util "^29.6.1" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" + integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== + +jest-resolve-dependencies@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.1.tgz#b85b06670f987a62515bbf625d54a499e3d708f5" + integrity sha512-BbFvxLXtcldaFOhNMXmHRWx1nXQO5LoXiKSGQcA1LxxirYceZT6ch8KTE1bK3X31TNG/JbkI7OkS/ABexVahiw== + dependencies: + jest-regex-util "^29.4.3" + jest-snapshot "^29.6.1" + +jest-resolve@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.6.1.tgz#4c3324b993a85e300add2f8609f51b80ddea39ee" + integrity sha512-AeRkyS8g37UyJiP9w3mmI/VXU/q8l/IH52vj/cDAyScDcemRbSBhfX/NMYIGilQgSVwsjxrCHf3XJu4f+lxCMg== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.6.1" + jest-pnp-resolver "^1.2.2" + jest-util "^29.6.1" + jest-validate "^29.6.1" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.6.1.tgz#54557087e7972d345540d622ab5bfc3d8f34688c" + integrity sha512-tw0wb2Q9yhjAQ2w8rHRDxteryyIck7gIzQE4Reu3JuOBpGp96xWgF0nY8MDdejzrLCZKDcp8JlZrBN/EtkQvPQ== + dependencies: + "@jest/console" "^29.6.1" + "@jest/environment" "^29.6.1" + "@jest/test-result" "^29.6.1" + "@jest/transform" "^29.6.1" + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.4.3" + jest-environment-node "^29.6.1" + jest-haste-map "^29.6.1" + jest-leak-detector "^29.6.1" + jest-message-util "^29.6.1" + jest-resolve "^29.6.1" + jest-runtime "^29.6.1" + jest-util "^29.6.1" + jest-watcher "^29.6.1" + jest-worker "^29.6.1" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.6.1.tgz#8a0fc9274ef277f3d70ba19d238e64334958a0dc" + integrity sha512-D6/AYOA+Lhs5e5il8+5pSLemjtJezUr+8zx+Sn8xlmOux3XOqx4d8l/2udBea8CRPqqrzhsKUsN/gBDE/IcaPQ== + dependencies: + "@jest/environment" "^29.6.1" + "@jest/fake-timers" "^29.6.1" + "@jest/globals" "^29.6.1" + "@jest/source-map" "^29.6.0" + "@jest/test-result" "^29.6.1" + "@jest/transform" "^29.6.1" + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.6.1" + jest-message-util "^29.6.1" + jest-mock "^29.6.1" + jest-regex-util "^29.4.3" + jest-resolve "^29.6.1" + jest-snapshot "^29.6.1" + jest-util "^29.6.1" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.6.1.tgz#0d083cb7de716d5d5cdbe80d598ed2fbafac0239" + integrity sha512-G4UQE1QQ6OaCgfY+A0uR1W2AY0tGXUPQpoUClhWHq1Xdnx1H6JOrC2nH5lqnOEqaDgbHFgIwZ7bNq24HpB180A== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.6.1" + "@jest/transform" "^29.6.1" + "@jest/types" "^29.6.1" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.6.1" + graceful-fs "^4.2.9" + jest-diff "^29.6.1" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.6.1" + jest-message-util "^29.6.1" + jest-util "^29.6.1" + natural-compare "^1.4.0" + pretty-format "^29.6.1" + semver "^7.5.3" + +jest-util@^29.0.0, jest-util@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.1.tgz#c9e29a87a6edbf1e39e6dee2b4689b8a146679cb" + integrity sha512-NRFCcjc+/uO3ijUVyNOQJluf8PtGCe/W6cix36+M3cTFgiYqFOOW5MgN4JOOcvbUhcKTYVd1CvHz/LWi8d16Mg== + dependencies: + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.6.1.tgz#765e684af6e2c86dce950aebefbbcd4546d69f7b" + integrity sha512-r3Ds69/0KCN4vx4sYAbGL1EVpZ7MSS0vLmd3gV78O+NAx3PDQQukRU5hNHPXlyqCgFY8XUk7EuTMLugh0KzahA== + dependencies: + "@jest/types" "^29.6.1" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.4.3" + leven "^3.1.0" + pretty-format "^29.6.1" + +jest-watcher@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.6.1.tgz#7c0c43ddd52418af134c551c92c9ea31e5ec942e" + integrity sha512-d4wpjWTS7HEZPaaj8m36QiaP856JthRZkrgcIY/7ISoUWPIillrXM23WPboZVLbiwZBt4/qn2Jke84Sla6JhFA== + dependencies: + "@jest/test-result" "^29.6.1" + "@jest/types" "^29.6.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.6.1" + string-length "^4.0.1" + +jest-worker@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.6.1.tgz#64b015f0e985ef3a8ad049b61fe92b3db74a5319" + integrity sha512-U+Wrbca7S8ZAxAe9L6nb6g8kPdia5hj32Puu5iOqBCMTMWFHXuK6dOV2IFrpedbTV8fjMFLdWNttQTBL6u2MRA== + dependencies: + "@types/node" "*" + jest-util "^29.6.1" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.5.0: + version "29.6.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.6.1.tgz#74be1cb719c3abe439f2d94aeb18e6540a5b02ad" + integrity sha512-Nirw5B4nn69rVUZtemCQhwxOBhm0nsp3hmtF4rzCeWD7BkjAXRIji7xWQfnTNbz9g0aVsBX6aZK3n+23LM6uDw== + dependencies: + "@jest/core" "^29.6.1" + "@jest/types" "^29.6.1" + import-local "^3.0.2" + jest-cli "^29.6.1" + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3410,7 +4692,7 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -js-yaml@^3.10.0: +js-yaml@^3.10.0, js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -3418,6 +4700,11 @@ js-yaml@^3.10.0: argparse "^1.0.7" esprima "^4.0.0" +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -3438,7 +4725,7 @@ json-stringify-safe@^5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^2.2.2: +json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -3484,6 +4771,11 @@ klaw-sync@^6.0.0: dependencies: graceful-fs "^4.1.11" +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + lerna@^5.6.1: version "5.6.2" resolved "https://registry.yarnpkg.com/lerna/-/lerna-5.6.2.tgz#cdcdfe4e8bf07eccb4ecff1c216def9c67e62af2" @@ -3513,6 +4805,11 @@ lerna@^5.6.1: nx ">=14.8.1 < 16" typescript "^3 || ^4" +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + libnpmaccess@^6.0.3: version "6.0.4" resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-6.0.4.tgz#2dd158bd8a071817e2207d3b201d37cf1ad6ae6b" @@ -3613,6 +4910,11 @@ lodash.ismatch@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g== +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -3636,6 +4938,13 @@ long@^5.0.0: resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -3673,7 +4982,7 @@ make-dir@^3.0.0, make-dir@^3.1.0: dependencies: semver "^6.0.0" -make-error@^1.1.1: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -3721,6 +5030,13 @@ make-fetch-happen@^11.0.3: socks-proxy-agent "^7.0.0" ssri "^10.0.0" +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + map-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" @@ -3980,6 +5296,11 @@ mute-stream@0.0.8, mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" @@ -4055,6 +5376,16 @@ node-gyp@^9.3.0: tar "^6.1.2" which "^2.0.2" +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.12: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + nopt@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" @@ -4099,6 +5430,11 @@ normalize-package-data@^4.0.0: semver "^7.3.5" validate-npm-package-license "^3.0.4" +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + npm-bundled@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" @@ -4340,6 +5676,13 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -4464,7 +5807,7 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -4553,7 +5896,12 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -picomatch@^2.3.1: +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -4578,6 +5926,11 @@ pify@^5.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== +pirates@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -4585,6 +5938,15 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pretty-format@^29.0.0, pretty-format@^29.6.1: + version "29.6.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.1.tgz#ec838c288850b7c4f9090b867c2d4f4edbfb0f3e" + integrity sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog== + dependencies: + "@jest/schemas" "^29.6.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + proc-log@^2.0.0, proc-log@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-2.0.1.tgz#8f3f69a1f608de27878f91f5c688b225391cb685" @@ -4623,6 +5985,14 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + promzard@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee" @@ -4682,6 +6052,11 @@ proxy-from-env@^1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +pure-rand@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306" + integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ== + q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -4697,6 +6072,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + read-cmd-shim@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-3.0.1.tgz#868c235ec59d1de2db69e11aec885bc095aea087" @@ -4830,7 +6210,12 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve@^1.10.0: +resolve.exports@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + +resolve@^1.10.0, resolve@^1.20.0: version "1.22.2" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== @@ -4931,6 +6316,11 @@ semver@^6.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.3.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: version "7.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" @@ -4938,6 +6328,13 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: dependencies: lru-cache "^6.0.0" +semver@^7.5.3: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -4982,6 +6379,11 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" @@ -5028,7 +6430,15 @@ sort-keys@^4.0.0: dependencies: is-plain-obj "^2.0.0" -source-map@^0.6.1: +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -5092,6 +6502,21 @@ ssri@^9.0.0, ssri@^9.0.1: dependencies: minipass "^3.1.1" +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -5160,6 +6585,11 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + strong-log-transformer@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" @@ -5183,6 +6613,13 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -5228,6 +6665,15 @@ temp-dir@^1.0.0: resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" integrity sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ== +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + text-extensions@^1.0.0: version "1.9.0" resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" @@ -5267,6 +6713,16 @@ tmp@~0.2.1: dependencies: rimraf "^3.0.0" +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -5289,6 +6745,20 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== +ts-jest@^29.1.1: + version "29.1.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.1.tgz#f58fe62c63caf7bfcc5cc6472082f79180f0815b" + integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" + make-error "1.x" + semver "^7.5.3" + yargs-parser "^21.0.1" + ts-node@^10.9.1: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" @@ -5332,6 +6802,11 @@ tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.18.0: version "0.18.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" @@ -5432,6 +6907,14 @@ upath@^2.0.1: resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -5463,6 +6946,15 @@ v8-compile-cache@2.3.0: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +v8-to-istanbul@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265" + integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -5500,6 +6992,13 @@ walk-up-path@^1.0.0: resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-1.0.0.tgz#d4745e893dd5fd0dbb58dd0a4c6a33d9c9fec53e" integrity sha512-hwj/qMDUEjCU5h0xr90KGCf0tg0/LgJbmOWgrWKYlcJZM7XvquvUJZ0G/HMGr7F7OQMOUuPHWP9JpriinkAlkg== +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" @@ -5593,7 +7092,7 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -write-file-atomic@^4.0.0, write-file-atomic@^4.0.1: +write-file-atomic@^4.0.0, write-file-atomic@^4.0.1, write-file-atomic@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== @@ -5657,6 +7156,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" @@ -5677,7 +7181,7 @@ yargs-parser@20.2.4: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== -yargs-parser@21.1.1, yargs-parser@^21.1.1: +yargs-parser@21.1.1, yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== @@ -5700,7 +7204,7 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.6.2: +yargs@^17.3.1, yargs@^17.6.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== @@ -5717,3 +7221,8 @@ yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 6917f93f8664f7cae0abd1d971760f587c783381 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 11 Jul 2023 15:17:35 +0700 Subject: [PATCH 04/75] hard-coded list of whitelist pairs --- packages/oraidex-sync/src/constants.ts | 16 +++++ packages/oraidex-sync/src/pairs.ts | 81 ++++++++++++++++++++++++++ packages/oraidex-sync/src/types.ts | 6 +- 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 packages/oraidex-sync/src/constants.ts create mode 100644 packages/oraidex-sync/src/pairs.ts diff --git a/packages/oraidex-sync/src/constants.ts b/packages/oraidex-sync/src/constants.ts new file mode 100644 index 00000000..1b720403 --- /dev/null +++ b/packages/oraidex-sync/src/constants.ts @@ -0,0 +1,16 @@ +export const ORAI = "orai"; +export const airiCw20Adress = "orai10ldgzued6zjp0mkqwsv2mux3ml50l97c74x8sg"; +export const oraixCw20Address = "orai1lus0f0rhx8s03gdllx2n6vhkmf0536dv57wfge"; +export const usdtCw20Address = "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh"; +export const kwtCw20Address = "orai1nd4r053e3kgedgld2ymen8l9yrw8xpjyaal7j5"; +export const milkyCw20Address = "orai1gzvndtzceqwfymu2kqhta2jn6gmzxvzqwdgvjw"; +export const tronCw20Address = + "orai1c7tpjenafvgjtgm9aqwm7afnke6c56hpdms8jc6md40xs3ugd0es5encn0"; +export const scOraiCw20Address = + "orai1065qe48g7aemju045aeyprflytemx7kecxkf5m7u5h5mphd0qlcs47pclp"; +export const usdcCw20Address = + "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd"; +export const atomIbcDenom = + "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"; +export const osmosisIbcDenom = + "ibc/9C4DCD21B48231D0BC2AC3D1B74A864746B37E4292694C93C617324250D002FC"; diff --git a/packages/oraidex-sync/src/pairs.ts b/packages/oraidex-sync/src/pairs.ts new file mode 100644 index 00000000..de80754d --- /dev/null +++ b/packages/oraidex-sync/src/pairs.ts @@ -0,0 +1,81 @@ +// TODO: Need to somehow synchronize with the whitelist pairs on oraiDEX. Maybe it can be a smart contract containing all whitelisted pairs + +import { + ORAI, + airiCw20Adress, + atomIbcDenom, + kwtCw20Address, + milkyCw20Address, + oraixCw20Address, + osmosisIbcDenom, + scOraiCw20Address, + tronCw20Address, + usdcCw20Address, + usdtCw20Address, +} from "./constants"; +import { PairMapping } from "./types"; + +export const pairs: PairMapping[] = [ + { + asset_infos: [ + { native_token: { denom: ORAI } }, + { token: { contract_addr: airiCw20Adress } }, + ], + }, + { + asset_infos: [ + { native_token: { denom: ORAI } }, + { token: { contract_addr: oraixCw20Address } }, + ], + }, + { + asset_infos: [ + { native_token: { denom: ORAI } }, + { token: { contract_addr: scOraiCw20Address } }, + ], + }, + { + asset_infos: [ + { native_token: { denom: ORAI } }, + { native_token: { denom: atomIbcDenom } }, + ], + }, + { + asset_infos: [ + { native_token: { denom: ORAI } }, + { token: { contract_addr: usdtCw20Address } }, + ], + }, + { + asset_infos: [ + { native_token: { denom: ORAI } }, + { token: { contract_addr: kwtCw20Address } }, + ], + }, + { + asset_infos: [ + { native_token: { denom: ORAI } }, + { + native_token: { denom: osmosisIbcDenom }, + }, + ], + }, + { + asset_infos: [ + { token: { contract_addr: milkyCw20Address } }, + { token: { contract_addr: usdtCw20Address } }, + ], + }, + { + asset_infos: [ + { native_token: { denom: ORAI } }, + { token: { contract_addr: usdcCw20Address } }, + ], + }, + { + asset_infos: [ + { native_token: { denom: ORAI } }, + { token: { contract_addr: tronCw20Address } }, + ], + }, +]; diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index e118a51d..ac6488e6 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -1,5 +1,4 @@ import { Log } from "@cosmjs/stargate/build/logs"; -import { Tx } from "@oraichain/cosmos-rpc-sync"; import { Addr, Asset, @@ -9,7 +8,6 @@ import { Uint128, } from "@oraichain/oraidex-contracts-sdk"; import { ExecuteMsg as OraiswapRouterExecuteMsg } from "@oraichain/oraidex-contracts-sdk/build/OraiswapRouter.types"; -import { ExecuteMsg as OraiswapTokenMsg } from "@oraichain/oraidex-contracts-sdk/build/OraiswapToken.types"; import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; export type AssetData = { @@ -93,3 +91,7 @@ export type OraiswapPairCw20HookMsg = { | { belief_price?: Decimal; max_spread?: Decimal; to?: string } | { withdraw_liquidity: {} }; }; + +export type PairMapping = { + asset_infos: [AssetInfo, AssetInfo]; +}; From c60940380e896b2746e736a85e603f222e8fef61 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 13 Jul 2023 10:04:08 +0700 Subject: [PATCH 05/75] updated latest oraidex wasm and typescripts --- .../data/oraiswap_converter.wasm | Bin 205619 -> 205592 bytes .../data/oraiswap_factory.wasm | Bin 237938 -> 242119 bytes .../data/oraiswap_limit_order.wasm | Bin 322965 -> 321652 bytes .../contracts-build/data/oraiswap_pair.wasm | Bin 318065 -> 319232 bytes .../data/oraiswap_staking.wasm | Bin 336143 -> 336122 bytes .../src/OraiswapFactory.client.ts | 29 +++++++++++++++++- .../src/OraiswapFactory.types.ts | 8 ++++- packages/contracts-sdk/src/types.ts | 2 +- packages/oraidex-sync/src/helper.ts | 2 +- packages/oraidex-sync/src/types.ts | 3 +- 10 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/contracts-build/data/oraiswap_converter.wasm b/packages/contracts-build/data/oraiswap_converter.wasm index 4fd507317feeb2f5f9ce8d1318a03ab3423d40f3..feb16e33baebe94230f1057285faf84094757da1 100644 GIT binary patch delta 14182 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 delta 14280 zcmb7K4_sAM^1o;910KAGTm%&q_451&{x?mG-1JzcrEP86*0yblN~@Tlg8$kIQc5be zFwMhmtfG=qQBvVP+9;`|q%@IBdd14sQL0WS^doY`rSfoe5O~}!BQ%EcGD++x=FNex_IjX(f-Wt zt*Iieq@rGvX{Ih)v@F@kNX4d4wxxqSp!}_y2et34*No@Lj4iZ41B&S}!E9+7VcCF*NO~( zF;|8$88vUN6{Et>?0}RrLC>vXNOEempFH{c9fLGkPfG&&V9nG`k*aHfh%`OLE_(ys zG<~f4)x%;y{!~++R)Bv-nm!3;YS*#?VSpHZ!5?kg2pOMch8a6Vv=;;ns21wGZK_e0 zO(H8G0IR996`8_mVxqGWkWD=WO(#+zm!V7%!+bqeuckD*%@(}|pok#N_?sip6!PJ% zFum9K7&R$WO)$A7s|KztBh?BOwcfu!mQ z{KqsN6Xq-wjN}N$y~*r>dXWXA#Ag}GR!px&Gd6NHH-~?Y0J{EZ;yQLJ!zT5~Rxx-| zwMf%76UwLRO(KmvCTOX8BWe@uVm*q6U93gX%e*OcL`~TyQu9kN%A;t(ZHh3;X5sf> z5dX#i^zTW+Cr!9@6KM`l1tb?Z_A8?_y&b)zP?2Td6e=d;U&4og;mQKAau~7_?NHGT zLs`yVPsCUe2*wS}+9U#DGWybJjDgXkA|NKxGP~(9!82hV5Y$~|)nPn5Qx@3_Eg}$^ zre6mG03-l-^V4(_G_cGBie~Wuhz>shCM9eoBQQ6)t4SOfDKy#A+wy`j&yOL5=+UK5 zM<90t@df}!H;EmDlAO8(*<&IN;EqtwKAL0~*+*mTVztt@i{ue-xG~RX5?Vk90FR?X zJt1mie6kdC+e(Y2!7I0mMdYh`t{%b-#C5ZJX{Q+Bt9-k&qJ5|MI)B?cXyP^YSwSu2 z*(RV-j3R`EeB}VpSVj>HLRK@{n^B$7Vs0D9DB?lLGN#5e3Tq(MNl(^97*NRe*FcDp z>zE#8YSN!2i%~ZINYc03|R4Xb+#Rej`%Hvk}1HZ#7C zm1NADWmmMS(~pRyvpco5)a%st$LbMU|Fn8m@5Kkyo?T*qIHC^i5+jEda<9>HBZEk^ zOvsXDT(06j92-r~qfn!gcZS8E5!g6Uny?ct`k}Ln^MSf{TxIZ@0Bd#Hr(tiYVcTzx6Q@ zCr+vR9}}yo;B8g^o|vj;JT4ZC6Y8nQ#b#eycafoeSPK3Jk<-8j;{T_-a>zXbyf|BVs-1JmOg~4&Z`k#pRuQhQ^LPLvYKp zVzlnBewPK*Bk@3VQ=RB7D%Iz8;;P8%-EeE7EO|FL^?C8GE-Hh4UK9;_OeGvD<4lLQ`Czy#IfH8E7Z{i=9Cl&g^k#EfC?=0}$XRuto@ zko}JyB1OIVnz&vx{LgJhs#gw(^ASF04v2XvZf9XX?ktQKC}(r_$flV4Mg;d96#Km~ z`*@Zc#t~isgnUEY^$zwrA6ip?)AoB3;;OkA1N!u6E0YNMnfl}%ku1)ruy@7I@JhtD z4BOPSC7_z#6-n^b7w?K+h(gX~I)1an1cZExKBdWozz)sjBTUB1GXiIa|M@-u4 zMfujCir0zc8g<}Pk>qs(w?cjOsZfay*1X3N>IQF&q4w&0Eb5R=5OovsTz%n^p+2K4ASSH%YQwKW$>BEK+Z&EznN zU8`<}T^SwgvbJ`x>Qa}d)XD!7z4F}-o^Zu@q(^RM%n{>p#6-JdDmqxhkV0xgSi>CF z4rerUu!hTU*&Mf9+z{swjX*J9f|M_R?FXl~71foW00ZS1@atl7n+!`#Xh_B1!W z@{~(P^m=3=3$mK(z&3xXZaXb*Fx#QCZAWYgWsA!ri!drV9NUEU)8fXs_GjFXA2pHp zg0k@$bvAa10bj~2};>N_#~vgTcof|+D`vZOG>mKb{}S;N*%I#a1rlt(sc_WERngLM?d zu}GV4rR-*P`*&ibyI@o2f~}tWPUMIhHR5|wEQ%RisQ&f67=zsVA=`6aENjb?V%kkJ1eFcTd~=)Og3!;PPp}d3BGhz3?s9U zbMFWfh76+6fSs@LII^#FkN6Ko(nsRN-s5D59#ysVTB@d{X<^tJ?^cmS)1r2wE=AKO zYBB0QoyMyoq7rf9q2u)HlmsO3IRPkz6NPMW+j3ynxri4?O|VR-o&q-(s@g+|m<_La z=*q~}N>)r3)&>J%I6n_|6t#}Twz5Z{v8`;qoo>t3&Eb^WUCqzKX-7%{dK{%`=^9RT zvKyTgUXGfhusX_s(SlKnI-$Uf9oh>X~HU?!V7>1FWskJ z_t8WIzsFDKW2sN~(<{!FkH^-mwkMLc2Wp*<-_8yur1@LtnJ3?K(x#A_{GzcZ+m&NQfradf3x z7)$8`-K{7Tw)&X_)zME1Knl((>iJmubYjKJPN12PQU=8GAa}OeMYywK7vatdUsgwY z(hcflJYA&<<7oPS>neCGjv@u@c|I#Ad(q_o)`@8l)C-y7LsYTrhCHsS4CejLF+D&-7Xq$DWZC17}ZnTOURY5<5 zwB7#Sah=((AZ&2(Xg~UMc#^$|#3oZ}Go;8%<`XSeeqs9LUiD;vhW!KwZZ??13yzZk z%0jr@Fo@E__ws%;LS+o1Uh44qv{VfkOw9xBT4ReaqoR}f^z29O^vF^@OL|t|zA<(P zy(s*4yp;3m(X2igO1)E>ahNe4=3S#lBA-_<2@zDq&^`f$aN`IRoG^^;C3>hy)u+-K zz~BqRX=pfA;H<|LfT$Q1j6Dw*S5W!M2{aSnn+bHUGXU{K@cHxU*Frp@64R(Z4moLQ zloEzKK5sIOMiP5qGF1Rf_!%v7S8k6yg1b2T;#KYW8CsuFzKiGz(V~6@psu@!VwEW= zQq^BX6R7f#ij|bWNU|C;g$7f>A$83Z`Xx2L9zywS3Z{5%Xw8JFs3{1oSvQpiBdQHa zm#E`huH*8xRC-V?mNc6R52_%b?*uLwIPvjmvBxwjzRor^Pb@bD!1tU2$ht=8Fgsk# z>!(Fb01!bdwe(v{n?WKZwqJFZMh_0M}U`}6GPPOq8>Ye|4rg-H;4EUJAS3Ay_aKprg zaW(yphwbgW+`%rzx>os-IU20nsL-Y@+|=KBOVrua2ZC!#HEuffUfgJR6Y8lZRI!n9 zO%&>>DqCtOXRX62x^fmWXU{G^Eqg;%Pp5vkK{_~{hW#%$NWIjUODUBK538TOhvx%8 z>%jW_rSzNpb${omPLTnBJT#F1IBKJb{y0WochDq%Pez}1(0=|{MmIU=K)=E07k{(c z4)w<{`m%$j_$?s$GH}Bc9CRuSRUv<#Yehx~zPfxUNc3l{x?rjfUa#U{%-_0}BvXvF3gbjL?2 zydGhZXKXuT{N7N}g;t?Hm`mv){U%>S?+Skdn0W7meZXO*UrXaKz~IDdX_n~i^i=(k zGa_vF<7IVFT}O3ZI`)kfL)d?B&7j$FRo}X5_psI8`mL>Yvr3vz0bfBo(>Z4zQE8Rr>grc zU5=z3v52Nj<5w+yFyo__zAB)t3Xs&7^T`6|DZGJ!b_K^A4gdu<8qlVrLNm^aMUwI) z&PR(VXNbegL_R&OSuRhNzo=y;N8sJgl9C)Q�uuG=9t(CUBo&DdY|C6t`Fd+6?px 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 delta 67639 zcmeFadwf*I`9HohyPMtI4py8sZs8mr=K*1ttz0p=%b%Ua! zqJ|#ypp6QO8Wj};3$;|EqD?C*Dq2+3sI;O|ixw-^sK597nX{XXVB1fBpU;0^@?vM^ zoVh&n%rnnzF6Z~}=Y956-r5gU@N@llcaQ2-7EocO!ogr98i@phAr*~Af`|n0FYH$9 z?jfauYJI&PrLs0Z{A3_I5pY@_cCQJR&p$IV_oBIH{AA(Wz>mTg%subyiv#n*=l^8x z1s4a-ikx%Sk1m*vH}j*j&p!LyGZ@{evgXW9rQT9qE|`1%PtLkv?u9eYS~wS(&%Ge< zo+>!&$4Hzx`;56We>(Sq3(q?D$1_I+-gB)`-^7IHb^QzaMt!;Z!J(7hR`05JRGWHV z?NWbMe^DQ(-ReX2k@{Gb{7oHE)NR@ik341i#IN+pC!Tcpg!-Q?+@lt$9qOk)x$pu! zzi86QC;t5W8`P1D)YIx(wN$;1-|JM9`jzT-o4Q@CQR~&hwQ9m2QD~idR9&z^J)#nm zURE!uBcD-Q)JFA`dO|&^?pKegE&6V?N&N;DZB~z~->ToKhgGw>S3RhHtsYVjsC(32 zb)TB>shae6^_jX`P1vrN>Isu>(AT5j?RwJ5%k@qA$Yn^ldZk{a@6>ncMm>oQ{7(N? zZ`MaXt)J2Lf70A_87IueLuTSHMpfY zhZL_s5vR`1aGKS8XB!i1mZFkZ*U!r-R&3<*+$rj+^)Ke0p$5FB?64iQ! zWc>(j)UT{2ooxr041^Nir+?FZ*I$u;yz23d?qO?Zzn*XBI@NYydLQAzhJ74>a#+r%Qg(@r2Km~DsB-kj8Z7y!Dy z`>U$mJ)`&?{2o>kLj&HYKUBr;WhMD>X?|OPt$Yp$s{_*jW-HRcN>ZgFCI)6Y8ICp*;wV9cKRl5aq_M zBt*+&r{U4xW2g0OS9+XAJ(YS#FdiZd0aUs&IY8&e`x<1Jk@?D96)$52PsHQqd86d~ z-2H1@hB(su5`&}$8G7Vw?EOO)_@K9s16Cy1>rqipw9T$4@A56ia$+na+t&3!K@`Mx zW5rE?wojk4@OxvQ?yRA?&-fnbpho|lZ`JuRE#2RZv6KFX`i@awxK7_i-M&zcie-Uv zva`{ZHpzhUJNk|VGK%|6MU`ju>%%G=`&IQ7oVA)>2t{mFv0RjE!6RU$RT-*yr{4vp zW1Es4JFRjW>)244M!V9n4=bf(HC4%uMYHWJc8t^fw5n%vh;tv~VYjAFnR{ga@c`fZ*i}r!V?z(|RIh^LX^|@ddsQcxC zRcb$8RVN>3$m2t*Z^a+`|VQskXR{gC-74^2?wD0#eo+ zIyBDl8Dh`@meU!cXZ=X`bNdel3sUY&r*?N|3_eAJ27fbn2&qClQGZbWF?evlZ}6Zx zQ0%Dl>vKblmN@Z=fLN$m%j6TZO{p1_-=uRgK?SKfa7|6`Y^hMF{Bw6>O>Z{2v*vI^ zeE>DYxiWCb&-uK0$ZYkcn>F-MCXN{Dm#rZ%pSu?gt;A!`I!DiE;gC~2-_Bs+;$cTK z|GZ&B>LF(YpJRI8I&1*&{p>J~P3G{kndfK2&jj+{9{!Quz1&?kXu^MiwKHn3Qw{6? zR-2<#tJ_}JFZQHHheH`@`fv#}#M^z3bnT4Ej3n~Kn5W=hnvecqTPSOa`mQEt>S)=S65eq=4 z@cNrZ)o?6>jz1pTMimjIAB}tXI!3HKF*w5X&$G?t@iNmf08HEq;Bl9Q&ShDj`OYPxPQ1Y7kn7jJ$S$?c<{pk{0 zOpx;@Oq)e8%IodCdOKQw!3FgIAWuLks|R#;mZL5-Pca9Pv$P(w7Yax6X|6oYw6h&O zfiuHvb!s6Z$m!|k+P zjklDWoPEmqXENzFOe}*K6%q)L1e~p`Cv1&%*G-fOp(dS4mZzP)V4<8T-fwznvMBHN z!&I%B?TsqcJq$sTDP~x+z1gMO?g5deEA=K7R}?2XsDM0JD)?2YKE#$fA>}eCg24|4 zA_oQ^Wh#Z&dV5RtMW$X%QOZIuuU?I1!^hMsp$hF7HVmp*b;P(M*o`dm4JdA)=aAFH zt|)74j>^{Kz-%*}+zA+pkn`acc#$jEP{{%zyiOMIKAo%zRg)K+ppI7m^v;{0AUnM~ z5U}|dC#XJM!J~?@olp#dsy?@n9M(I#r#{V_Gf|bOcJH!@s=K>k%0TAt-%DR^If(#Ml+Eq(lztq<(u(3@jA;;o#lA|(fOcDdhKuVu0-me-xS zmU`XMwGyw_5$X`rzy9G|>BGk{EqyGF>pn&3W0zDPExy+|mUnJXeWbTCt}So!k!sfMbQ28lW>)Aq?$3|ws(0P&t?I5TGJ~~x40ynn<=zY3 z^|5LEQ-->;)Y;!z>fGD|LTBdjq~60PSM&MXC!b_`MBUe7j}AZONWNHhNETh7j&UAXIg1@7oGN#q~@%R z)|!F0whvqW%gXQe{xQtTRta(Sf4o~Sa{}~ z+49(iXaXOCPAY8N&aa1>djYvSg-ZLJ3F=Yzu(@~VHw3|-(1ob4yniuw1?fBtn<=j5*E9VT#x?JbvXT##i$#Jc7M@2V~X=Yegxkvr@NOi4y%a4zR z+5GX3&y>o>y35Y(g99H8YY_xXkE0AAuJ_l8iXa?iW4kkJ0LWc~D?zM{IM zmk5GvuUtRoqEa<(iKanlDhfKA7X#|Bg@?d<^5LQ@@i4-NsV0L?yPwnbER11Nl};RrYtDi{5p&F#JX1~iqpVg;Hi zUQEZt)Wz=~R8>nkIN*e^uJFF?s*m_D)U@>#nH;TpO-=p#L53DLo<)zN~k^Y0Ov<*A)Tl4r;S2VB-a> zPl%dkG50=k7q7UD%vU?xFv@UbD0k2;gK2!h0+1(07uc#}d3e-)Y#+X_Lxf5>di9B;}>&1C|)ot+#fwM6U&gJYT*$bd-)bS#AXzOj)87Ld@ zT%OFiT};HsZcl@WfioGwbbey=a!1=+sjy2TP|$xdT^?DHF>QAngDh8e3iq&lG4DuM(TxAgjpw z%6(+jGwk5ScUGyv?#FkQkiR+EP;5Fk@zR|o=6!GSKl3iP2ughu?3S3y>Bi|r)D>Cn z&;{O5W$s~jiR0sxyL@|)4xb`OqZN0FH2VErCmKwVh4v@z2CS|pC>N}*GPtJo-QU*_ zw~$Dv@1bKYDqhO`PprP))UX!M<;h8R3$7o&I}O*R#PzPb=M%|O?-?%?fq4&&UWpOB z>K@AEJMN{O_uRd`yRd(%Qs)5I2rQuY+k3m?UH4{6=+!eGd< z)cVrB;So{nnjbl)KQnb2WTr9Sz`1ka{epr;*Q=d{mSXE~J4j0}uBYC%XMG8NGd4^q z9c*X%6`10je<3qn-E%gS!j!ybLp^5DiyL~N#77&Z6diRd=y%^+w^Gd!zpOjhcARY@~9_O`@FLT#3Pe7z)j|_OrIS|FX9AOMd-( zRk&d@=wpBl?{_OA&3~W^+&yE{$>_r4o611|^aVie4#87oy}tgVP4_8vv%CDUO0~lM z-D4-JMz`p<6}fv?pm3S9V-<=&=T7)-g}TvQ@Y@UU`}S`Ks9(BSn@>@WX zjz?}(u?$e4w{n6mcc0r_td_aEHZN4m-IIQID5_lXyAu)L`@4ba7PtKIUZ~&)kH>p9 z+%Ju6UB^b~jp+2iz4Y-|k*nevz$0AoFxTK9!Su$+u7CW#Ea=mGlHn|I&wS$182goJ zCO-m9ev>XnVWi7YXiQ5bhHh>Poe5XB42?5K0bKl&wZP7R<(+N!u)Ejl9J_~;GvB(# zeY52y^_ctMlP9YsZuF`8u0DLb9`FIS9&=|tH3wO?Jk`HQJTlN=!gkht_=CSyc6V!$ z8+`hR?vH5!4R!!PiBfmTcB0nf?wqF=scY8%<>~uE>W1}=tvAamMdcs2z)$kmKQ2(} zardOHPeISU(*BP%BXyasst_ zDXcXcVFMoI65yNMl~TbIuPjnexz6^11=|($owmS&c!15qPO=tz>(0{MUE2*o_i=CA zUK-tc9|*Inb)DO?y)ybV6PeaI?%wSsP-C-Rt&YCUM_G73_SFf|O-y9t{q?U7iSA}1 zgg4K=Iv~0snfUV6W4c2V24Vu@F~u5Ajvc5DSkT92zE*@weB6_ng-Y0N0||pNKZ`Ee(iC zZ@?;XSG+M1>TTN_bD}FcT0ABCG84I|r6SMWEy108@J{g}NUSDzbkpM1T{-Lo?{`WefGrChzqKMc>ay1`xf z#R0{_Qf%W}HxSvNfB-$wsBr{wJ;u6RyVt+n-M#Sb1)!*R-Y)5~oj84#R4{IVbuNa- zwchOE_I~HC*z0sFz@mv`Ifdo%GsY5sC@g4EYCm|VpYc7i3Ft;8ZCJxo?ufSYjin8T zNEb@{N89KYa{tlhTiPw)^<606tKOX&-9rLV)_LxtcPl#*>)wSA%YF9Uc=VY&@jhhz z!rlAsP%aHty!SK&&hGc#hTHV_?+;Z)euhhP;4%(Ei)Y=ifB(FSZ9eknzVy~b>9WnD zQWC8W3`Y_WM?qw9agFT%^IZfuO50T)-1`@C;C}cQ0XXV3Le~*rN!bUZO0QPLYgCNq zfSqeyttf7RLGah}KNu7>;|_rD{a{4&#=8K0J`x`waXS;)Nc7u1GY-z2vvoSSvTe%SIN-SP zBXan4AJI_g`|+S;W0$(8emph=04MGV2nf#L(~@@>fc&lmAMecH3W#Pa8`{+P3ks%O~_{w|x?G6Q9th-S){M zKYhjD&L9cw{@VrO_9&>Q<09AhY14CY?%pEdsNs5a*p8%p*j)veIqa_Zuzjoozuk!H z?kM`$-g9%g${l1Ugve_@{kh(LtNX~O{nV508=qFGi`~#?X8`VbpB<*x-s(R18DG5m z8Kq7B-ya3=@BN4 z32hz6_m+8FD36P*DKr4^xB=qV=?T;=h{HsPLIxlXENHIfXE9KXp(0`l1NIq@MrQ*HjeBzUd`8IJhw3Kma)0Dc>Ak4NVw1 z@c;{6iXps(hHJ|s48M7VJSg$IZ{|P;i+_7`##%IoG5zVcv+-cZx96%QH%(GY@cWYb znOfpaR_bSCmna)6mQd(uHodnD@C|VGcqk`YCOh4E@oRtQ9V6biN_G2BMuRire=;0` z_|D^D@KPjr0X`6^WUWP&qq%s9Cn2XXL=DMUL&eq*nqAF7AQR!5pyWYp1-#`w`KSqx zyce}Pgf3R3;Y`R7rzdIp1;~hW7^=K;G9Og`Fl|Jgm+YSP-hnkN1b7YEh0IQ~T&%UTv1@&BVB))#T_F z`bD+nc*~AfJz$H=bmOwJlm4c_8`KwTQ~R=17hpakTU8xsGkfE*RXN_CmaVRVwSoOy zd1gNsEhY^{<4*eJLm38%Zu>9KhL(AjI|09I-3i%ipyQCSE9lm0c`SQHTDltP_U4ZF zCn4RImcA0{mbCObq#M)HZy~+y-j4E_H^s6yrKJx+dPQ1#Hq!0)bmYGt>9(}=Z*Cf@ zH{9d?^`>L8_uk!+ZQ{*nCM|s-(rePvcObnaE&UwQZL2%Ve}Uz{4XfQA-U(n?=X>MT zZg+z>b($FxPCBc^rw{tBKpn<>MWom#7uc_$Me!za5x;|8zO%JMdqZ;6+3@sn*A?6( zbYvhPYn3!-9n#>zUQ3QFj{Yr2#mS39*&teGjf0T{q%1?jU-0~(B}-piU80Sd&lP~C=wFSq40pv6f@pi zmxsyH*DzA6c#YVT?byDv}m#&uCi%iC+{qRt(X|#)mL{_PN8hhz=i^mPm=v{?X4|PRcf{OMgc%r;Qf=SyS$1* zb(p%=n^y=--RWIZs0QQRZwl3d>^s3xIds^s6^ zgqW1G_9`%rn6u$(UtsP~N+n=4V+k`1LBx%dA#1pIdl3j|f%kloDy`gkLrb7C5U33l zW1eqELevOq4qKB6a)2$K)lJp(;@EJ=h!~+9u96B}iz?^M?529E;ofE4RQH_4*s5Z7 zb6MAJT-6QSU0vgS*d6;@uk#v<6;`CYr;61Kb@|3#C8|#S04q9vetIvqt9Xdv4(0D3 z-A1nj7IXmt9=9maBGgcrII=k6xjj^0)!;qWL&a5{_f`)TL-4H$Vx{V9waDvLrmn_t z-CG72w|LK%DX2MKxLh59U~0K~0d^WU!X9Y@k_eC$F?&oA$R1OKHb8)4=UvxRovMa! z{II7wN#*%Ci5i@EQ*G5Jd*96%h^Vc*gaYoTr3F(kiVNd;kabS4q~4}{g?VqFP8R~h zH{R&uzB>|2k_iq(Lo&gE*nflfj;(6Uf;xTCvWKB8ru^mPHhEb)IT7pphC>Ah7y^}N^6TfK=54l^oLbvI%r0<@U*6BvA5xW~}DqeA^y?f3Hf z94t$oWI48vdP<6C)?+?r)yMMb4UcgAy{f+IZ0z^AxvyI1uSIW4DN39mrdzZ6DSsPh zH)tY7v>LS58&+v_GT<@2Dci^3 zqRM1b#b~OKmgskyDwL++SdgYZtxPrrb)e91ssd|4z~Zzjbxe8dHK3lbRSxhVfcGiZ zm>iaNucb<@K>rcMyr1`1Pjm3b4glFd?wvjWkPz!I46|rBnmtd3d334g8j==!5ljZl zNZ!i>)ae=&RXb4hyi*3MQ6cXB^}3v|3r>84VB}!E0{kzvzey+eH$Bg{+|h%Hn0gyb zzV*8x`$i{j*xl1`n7qNIfQ8EX3;IuA2w9V@TX} zvrseKbwI})2_1gt21tiZw>( zpWw9Nwe>8uSHGs6shWJN)2x0_r{QWNO&P=HK|Y;D%}Pidf#GQ{Ff33ZS0$u8!zO)CHH7;E` z$x5pDVcIy@AnU%mbfn(vy4LkG4}6!nzW-sqN*;EwrkAdgqr=y$fF76pLuaB3@-j4z zt^^g_|H^svS|C>Tx;xhu+09IuJ!@j#Du~AM@3t=c>dJ?9Z1^m2oO}LtpkrnVLRWvc zR!z`LZz5Q!-Bn4DfrX^vcK}r0xI*2}d$Ug2*&S0VC^IlKwQ106tW`y&sR;>1?7Ney zrB;;`w-K0-H5M8|YDxw$CEdL@_RdVz?xJ(02Tssb?d(_I-JIUR1FOo;gNo^FqkJp0 zKJb=}P<_>>o^zTWmEGRh0?Gx2S=+p?N2t>#a*UyprJse!?i%1DDi8=noM*#Ti#XL( zW)^xwg!^z2z?Amx9H}PB8YACRK|&}eE3$t9{l>ljIvfO0v}giWF}qH6P!>-SgI69LcosfP56$~ln+DED$;!;5N^scZTI&Kh%;|5wdqSCTJDBb|J zoSi*X9(=clWu~xE!BMoM$0#ya=>+RDm)SZ|P zVgb`v&74BUP~y4K(6gGnuSY8^wR=@#RCSg=D119>jKaAMZ}}Lh@7H70hg5-9{ea1h zPyayWa#_nyd;1Pm+l!v$_6xECk^!LuhNf?PYpfa}S;y|v$95uv3mxUKj$b3JBy$6S z*g-NkCy?C(gXh?wSFGp|6|jz&dfe+ZUL93%wwM?g14HUiJr;P0@#=7Q%k{GT{hjOM zQOa{b@VT3LLq9ds9dg4I{9bkgk6k>7Cr$3FH(Ukp>x>&m%Z_3UEu;f{Qg_{WwD8zqc==`_A3C%%;w%=_JcAilFQZJ?y$uGAFd*YXMb;w_x2rle=^A@9iW z=2SSz8k2(e04G`c8`FV*y?c3Fr`^ji3etBkdkc?M-C(X?cdU8~`r}#0scmuWwF^4j z0400v%#kIe%xdgl$}Lbc|^22urS6aG~}QR zo`*f&m{bo=Q*#QFYERJNwzhz^N9AUF?Q`KNJ?e+b-&9PESxT(`p)ulc-p*gG#F{-` z!HBCk0Y=<-W5h{KFtElOKLcgJ&rVPkusfPgP}R_q|9ApU$NtlM_(WALa#&%33Hx7i zL=f&d5&nz)d|B)bny#?54^qd^k;@z#kDsp2(I5nDyO=5~edyEXkdeiRZjuE9%G80U zsL9>1aS?1EEBEm4R|lx4AhF!L?i5uMrI!-$H+fH=qRv#Kz5X-QDL?Qo3smo%E$LRXNe7R2cedU2r7dcT3m4%j|9RrQknO?cM;fW*bJnBuknsp_cl|8W~s z4>~u33U|_lpVm-cR_8YqV1x~sM=J;=o;0!PVW+9}DPK0I2s^Sc;@%ghsXEz=j(jcj zf8ZdBDZLi5EFVjFP2QB5>Mk&hy)zZXqL)1j+o^=g3hIC zcl}&-O4?}n^UwQou6QyBo~int$tvJin;)Y#4ELInjbpOViZxoibY^l@0vQ@f0bmDM;gnSgJZg}fh%Dx;>hU8rZGQTy zM9^6o{H_VK=nkc;6A{@voVg$#G<%1^lsfnsJM8l_?!5&+Bf;#52#?yQ;=Ks=e&r3( zx+uYD#H_HxOK%%wkHyc357;uafXYd zvEHm?Cq0nkn3v5i8@fr5Q0ePLL-fR>5#_My#4) zLq@C*!}f3tWw}+_Y!O+m_C&J!{QxAl0 zccd;6T;eC#@diK6&hCeOPlJ3LP6Q^G##<40S~H|+r!_R+>EQD0qHuZmoe9sjP_1r8 z6>O(91bq3`t!0SJSb50E*5eX{^FxJI>8}FZ85N9`85ODGAfwVHeOQp&D)E->)Foca z;VSI?^;s27L^up>pek;-fRPBSw2`jp4JKjHy;>c3SD68nalxbHh)6l3D(qC?HblLO z6{1RAtEDqkISNx%Z}Y201~n=GdaRM0?P0&2m%t}MBsj;-5#1A+A1guF8kvuyu(5s) zjsbYj{zMH)XrA>3RSh&m^>ttZrdgB^VNVC`oLaq=D9JNG*jb=+c%fq2$&5wmuQXYM zfJ`W>)d!MY{R2Tj*eam%C~UGZk}iPzIxsQ8HJ5+UN&#BI)*g68(<?W{rm4jDz2^{H$Ro@}*fRN9 z@=^@|yFZLw3!w?Q1#Gc8a7D(bz&{Kd)5|mJ^kzTag!mRez8dkZetZq$+ru{62k0A+ z*y+F6jQB2zLsVkR?;eQ{4lK$Tt@inOmLksv#D)8V&j|Myz5}k0=hc`HqWp*wR$zvnYDUN7;M#!)M_$Y?k^_u1!F@)qXYLL?z2K{fI}NTPX%2_#bkuH zoBYDPnh^g!M#v51*3mfEYKlGpm`u?`6)=Sk3xlizrt0(_KfV?5eSUm9;_ZHXC*q5x zU5q*6OC=7`ijFP!;khq7S`i*WF7oV@vejsGRNxiHfegwkI{_?gX9P&t!4OEG@Ul_a zR)#=E6Oi!|;jOwgwIxzl6*>p0>c{sXo><{0 z+9~?{_+s$yWj5$U}a!Kq&9E|;?;pQ5m^V| z!#nU$bOkJ-B=v+9kvb5tX}dj)<41ZzZXQ8Ma83(F9hB3eLzsJMbDe8NP#E?bHZSB^ z*deLqYz7$wjaJGEg0?oDTat-Y2iB#8&`Fr$c;@rVW+`T*&>9q`ub!0Oti)ht(UoT0 z(f#VcDiy=FL`WDX5DQVHAPnPwKK>VZpT4LHy_RcKej>w2mkd(L!K6!4#QD-C5X+== zapdHakuMogPDQ@(ybnzbHh&A6XSl+Egn^6r!mLBSY^t>4 zJ}IIm2ns8nve}JLXr)kq2o##ZVa%A;{>p|Oif2+-iI7R2yoWGgLveGAmWd6CNUV|( zz#}};&}!FMcRLJ|JypTSS3{)bTb(KZEun%)tcoGZb@%H5<>tnU?fiJ4qrAU}(WyLV ziB6z`{(c3;eg&}li#m$>H!O52in%RKVxdex7~q!_9y$O#octYfbS1+cwYrI6|609* zA*TFthQ+nIF-jg^g_8$-0G_afA|@n2n@jn+c~MBZ0%<_80)bNd)Df`3`-%1 z7#7q*#YNby7J36h=tVmi*4!Hh=iDXH@%EB?=(s*8R=9}MmT55h9Ep-D zT?6@;@Z*(`n|{=WIF-y*x)@THQPk0nI`;VSeTeU3oPv=dhIczc*=uxJ)YO>*bKKAeK%_G>C|%3Wpv_8W;UZm)i*&?MQ}bZ7V~_s@6yv zHppd7N!kR4VImDGqmXFh6wWqNcx@z(GfY1Pa5UY)n^2uhC_v2##S5>#XIurvQ2v14 zJRBN}|^)a}z>A5g##@)a{j0ePTFI10x%L=@v6f+!k9bd|}dU9AHYDCCV zH9q+8(yV+QpjHQ7j5yZ93ycY|Rg`sjQ~Yjh^I9%fWudL;qt|x1DoGeVY)sbVEK_zr zj_MkWWPnlj8Y@y61Px>QLtVbQBZA-{I`gx=gp?2~IsBYy;1K3y#QnpbKOt=;~^GHUAbIiuedHJS=my-k^Qa0z64`0=Mr^XnbOU$?oVV6)n4mxWJ8b8W&>abK98NGv<4-dFO$g0jE+Xu5ui{ z0*9~zem=9Fktu2@-RT`%zZ9Y%A?%%}X5l#EIwu%+mJq-}kcF`#7Qu_fOoFHrw*`D1 zVlF}CwbScB89=W>(#Bqt5fAt-0EEWcHfBClOn84qw}m%YFb6+?uzA!$n>Xozgegl0 zB&{Is8WL9>+qo#sPsg^a0~e>c>F~sN)A6o=E~yngj-^@A8-*o3F8;CPJijgtTS#O0 z>FulzoCSh_Ks}$}(HlTFTt421#ZJP})|>=(ZP6E}jI|^-HTb#MH1JJlqN)Pl#DQ3~ zOfq21A}$an+n-I+s}9T}7?f1WTE$dWG%?kDgdJCblX}6)gtAjs=@VZ;#i~mg1Xn%5T+J1 z#fY7Ody*`OM_Apnge@EVCuJ36h3EfOm%xI(HbtI`P$52ls=C z<_Nz|YXp8ByryXZs{URl*R9f~@%FDk{Ck~TWRXXcr0CUy4+xXjl!Xrn9=qiPg|H*u z)eU5OXYbr+C=}aU;khqb4W%5ozMG4lF|K{s++drUYE)?Hl$N=J75c4M1rrrNwN}}R ziA3%L51DAc!K%RXaJw}N{wo99L$!g2g*LW^VxdG17elI14LOkUS3q(oQT@pMz&esj z;mU9vwVG>jxmKNIW0TVO5ZC0Y1CyOkz_+lfVR)XY<}2YisY)RJph9u62qHC1i%i&& z<+CHBK#<^RF8Qp(_o?lGFDsU3RvS#+qPHEa7S?8(?63u7W?IGvkg#)ozJM{HpMXBQ z9T8v!O@z2*8~0aYSz2xxGJ9~X7+oq7HoP=my&|gPLEZqdpTv6s!n5khq6R9gc}3W>YxU zmFW#(nkq1^!bZgg%NO|zmJe@Wf)~sMxksO)i7^q%aExFYhk?Z^y2wUN2A$z#%O%v5 zhDK+A7xiIyQ3a)NBpVJ}1!AiMGejQ+tGuWWNEEtC!z3^gPSC#^u%keTu~FrAPjhe@ z7&5#%$=2SDP_JlZy$B71gZ?Ip7xXvj78Z~2Ue=0|YQ?SKx=FPH%!6u$j18{!>yq$Q zfe|ShHXe=Yz{rED71&^r)GCGpZ`ekGwrWxuMWtU2v`0~Zs{>WALFjlLuVXd%KZH{( z=(`0G_u5}nS&7VpsT2Ov9@L2_jeALoIw47aYS|srFh&Ga2W)~sB1u;2YZE-GLTw@! z+Cdv*do?p;9f_90@B$R*RsTML`_OmdLzkU(TOysIL8u!2<72e)+a%L!bLcHXI!Le z)1-R7??y}$*frw=Hpb$((m2F)XPp2Q4Z+(Xd|MYZVm(bANEh)#|AmMbB_E=kYd4T? zT-_GwKD0x+`)kD%X#e}tJt5*9L=$vw#Ui@U1!X)D2Soj!i}*w*8Q)pN6B(UEJRP~; z7480wAEf7EL(hzsq)B$LAYX|%h;%n=l@Ra$N77v^1tZzX6^*S6R+p6SiKKWJ=}ymJ z$e8@xC1Mt_a(KTSmtZoHVaC7hEW>rKjT|6D`42uzvH zon`g`#mQ13>^g9QIcx-s@3)6CDAN{*h1i2vma?TDt1%*6&`u&xj=0zu8#^4!=5YB@ z0Rqr*{)>U(OKtxshU~RVqHdM}Bq^)c9B6gW0aX=*Q8knyP*6kWHe`H49|!H{JN3}x zCz&BQB=ZoIPKsSH%<2F=u?XOj^~G+~5SkC8e;lM-9-fhY zex8O665i$`@*dK zB47@(tbem9Bg^{B5jPeO){!c|v%!hiZ^GoE;AgJ&fXkG{QhpGf#c&a&TM^&o$G0QC z$B*wse4iiRg?PIk--GyKfuRz}z|xh(6NAx#(TV`E(#^=UT?$mA(NTft7zaXH7y=<% z7y<#U41t)f41t(!41tj4K*%2umwjrqnOpSy)cy{q5z`sNx=Hh%m!Q*xWFjUuud@Pi z+eBBITr2%t%_iCGCpRHE%#`1NXm3+yEuwJ~-PgcVYg|}!S|v*R+gT$~SzaS)qcT~t z3nqx|I10$LMJUr1;SQF9Ft^K0wGD`tOVq9~M!Q|@o8f(YW4^C3ugPucs~}OtATFFG zQC^XOU6M$X+q9snkikkU5yBDcMztWGRS`%sOa})|VC-W9m4>qG=*gui)@f(?M-QWn zRcYK5hMmM!!6-Cl1XgSagJR5b1a@o~*k1-3*oFo|704-;C=X%BaY&g53^RCpo;4B& zjSHcQoGpiG(Tff?-F zgt3W3oiTYdgYh}>dX8- zZsQ5g@poo^p~Clx)d(Kx^)G6afs` zLx9(8duS@1AX)RcB#&K3a6e_p8g06p!R`_SRJIu19uMBO5CM#-vk^4lUkl=w`0)l+ zr{|d5GaRfBnX8Dy@KrTnxBTEhU~~Wj(govJO@>pvBjkpxLphviyDAv_6s#&Za{zM+&~8~YcTSdHK`c|{TzL<6L@)E z97MpGf&AwoK+bsxkYg4Cyqiu(IN1k?#Nv(;(7!g99>F~#4V=fuR)9=o1lU+35};Oc z1a_rA-L&;j3mP0XyEMPaTA$Aa_$5e z{Gpq#Y%%|Qx`hYaOdJGe;y_yQ zfHf@x5j$n%?Gh^_vzJ&-U9gxjl&QttiSi82Jj=u?CB(z?7R{!!1zj#8ZN`MaYg0|NO159M%QW%F;p{2;gSW6af_d7 z(WJ9hrjVx+0bbT1z*F20$>NQuiJS(}W~m9vo#bZw)N#td2?(Z0%qV0G2L&Kn&VI>R~qFe%c}s6zzND_n~N2!X@p=tITrfrgQ_Qk$HO}Z zTPz&Hw2_VjO+b&J31~>7ko`CK1-6p-1&+Xmk~qQv4MFKjhRC&oA>KFgtS|VKKm4D{eUQ!86bq?*UVj5L>Q9z5ZDn#DC4_|S*&Bv^Z< z`^7Z1u!~z`gFhmeTd}Ts93unqIVYI33jSqefsK819Jf4zr$I(7h$kRSVCh=#PLc$wMXPJM%4^2rn4TH zhN+Sq?4`g2bwX@a3Q#HB$1tfCF6K)nsCezc8wxEGh^z*8K{NH81@ zvO_Qe+cYI>GemTf`c@(?bO&bVfWT5Gl5k+3*Rj09iP(lS>8*nP-^VaXeF2R5XW(kTANK&$_0eS4_*pd%`0QVy9BrvA2q}dt(MAzVU>jpGu!^Wv2hCh!Sj&l;nBTufY6ip{j&ERU!{;J#?3Zja2u0!GHb1%- z^BEo*;yDPOzat1`U@vavHrO38Kb~QMUU_FZ++t^4tM$0_Kf$lTIb|^QGk}a0fl+}O zrchXnr;s&JvSRv6Rr1s^jx#Eswk1M53hFfPy@3y?;{F^x)(#b>CWcOeKVy#A{rCY@nSg>Qxk7gSd}knh)^s!iAkbN@o+jATIdyu<_dTZ4dXU^uvJUpx z2nhBRfI;`2;9dKr8agKLC_)88E#KBLpV-gx$Ed{A!SXkyeJ%X2zU4R4MR}mmojCKrMMs&l9=;$To=q$X3fSh7P8JAr{ zFcud9$KYeK&LhRA)#@5nRHdf~xr57%VAZ^9n`f3JliY>QOy(|hLgT!?v;|k!V#H9j zH9$a0vUuc+f@_no5CsJ{`_W>^BR>i%ToI%Oi)vwd0f)3Yaasr^M!;ePPbE+oFgU}+ zCN=fRXf|=IX#SGap-BZ(N*|L=NMi9u8iJSs&y(2zZFPghq1Ob7p$6tam=uwFH8v+C z$a^s-+n5z|f-;dyyDdzE#wq@o7WvuTE(Vjs#&o`f1}Fj+n@Ok?k*QFKIyfV;8zCe? za2wNYYJS{`lPu%H9&+G+z`VoBWB7!$d)L;2L{1!Qe+%Jof&FjiPA)}8IA@PS=(GjS z22;&&Uc3L*tjSJrVJt^OP?6{y@H+B`Oy&_64hIFws%SG9e7vSj3YLt_xQ6RgAxx;m zq+m0TF^6*Oa2AzXi-Qw#pM0S>K`~&2LI@T&IWK@GVY7a~g7$pf5^%R+g#fq1&{;uu z1Uv~;TH@qZT6mPiQhbY>vkh0%cAd5XAM#b!=}HqLr%PBfD5z-J644By?ui#d1n6Rq zE=J;>eI(AgcYil!EBZs(N~^YwA+W*G7CDq@oj|%+38^VE5QD)Om7_9(Pi%%c)Ctg~ zbJewM1e}_x0&pZ3Op)V+*$AK(2M#T50Z=FbR49}HfB_Zm+rk`#QZWos)Fo8epfGf3 zm5088Z^VktgVZ%f6(KCj;JMt9fOqB!E)n$BF(USQ@Hd242yiJMszIdCq~&fU+?jAJ z)=8&`roy_`gvc~}2^-uLqQbufcrfyobAu9>QekZ{VSjXAd?}u z%xZ?v)NqVT8wCuKK1~7RB~AQHXIJypCFAmetbw|9JP?K{VGSi5qkfI3#w<z&x*HHUKxKERiOWX7LW_H(b<6kbD#Vuj2B_! zzz9*r2?v>WC!dsSF5;%YjT0NZ%6vx=rKt58R|xzQRE9daEeK`GZ$VE3{T7g&S-$_) zUyXqOh>nPr!K7on1$GX&OCXM^M(1!wzHvnG(^xPBvFAm_#j(TqHn3Xq5a<%EqE1N^ zMhD3{XkP3VwGJ{w-t85;Vg*ykLtX=#uR#DB9$~VypeE=OtaOLe1aTp`;fR20OC}ML z(&{}We8NmLqPr2va4fX*$|VHXR+jz*v}`Gt+EgEgcoNV>AoOF&}0 zn}}{fbQXFEQAW2PxaV{P5OC8F6yUJWRMZJg6Ti58)Vd1r2rBU5K{mb|-wR^6LdCno z*MV3I?hc!blMa&2nvAdkR;$SQR^mUZ4 zk%^IBxGS+DSU_P>2~k+EgTll{s_hDyn(1Z5c_hP*>nET;L(G!|uv8~zu#iJy9(5Oa z4(B@c9C-{QK1pm-MY({39)7BTvjr%Ebr9lFwFZX3Cv3wIcb3976g2N;2vS>tc6Ru2 zSlhvEetZeytrEvh=uHqP5N|Rf`x)Z25hS5h(146Bew^!S5PNtIV*q7fm`WZhI?(Gw z&Pog+$1KC{juhscRm;39eaeDnz=!cn*u)`87P1~(V zun4X~q4dVtQCZ`Z5E@;GQgaRS80k&SF}qAMNbhF8LYCDC>4aX%PymKJCdLJ~GL&%( zf*erqMUSwSiv8G--hPiM$boHNkaw2jz&_=8%e55j2a)3fs*ETE9hPb(4fu3|f;43Z1;*Ib8*n+#I|7&~js2na_>5>aQ~1il3>lq4na9ysulyhlhQ z0bV6^fowt($bcc&6uf2>moNpU7?3kS9V~2;X?>gULa=Slbh8YxRdJBW45bP5m;(rQ zC~S&TUx-RtP8_@`IM`C|6b!bUXcf}i3GWZrrPy=wZ8UP8-!|7r(Gp>xIzh5xo6{5> z@2mj;0GWpUb`xoe4g7-<8RrANNhw}O;@~kaQU^Cy5E@K$QDFQT2@3|Og^~!!z@%hl z2+YeL5}^}7hdlyBPeQ1Xc#+PqpzrsR`H%*k&?Ox^(lk`5w5FRoH4Ux!KWSQ8PEaoV zyQXtf^zQ?KjSK8QEH?l_<{F2Q;j>pC8?6Q;G(v2w?EnxX1^WxSv)}+LV;JNlQhY!R zPLT2Ya8Uq&%=S=>nKEMrMMQwv4Cg#ps^9zuA)+=vwMNKhho4%F$q9XU8DWIE?!FTaUTpBnNbVm70u6&k&i_CAgA=K9M+a^Y_IC+!+tkfyhMRD>#c4Un zUUurpmKSix9Rjb}q4So+%?_V;7zY9EVhDm_zZ`ts*ZG3(7H(tT5lLW?-m+PFRj`dB zUiLFWO+`F;SJF1b<$MV8v?AV$JR)BaCjG_OJjs1OynUkM?kXJcz}^hpP$WcpRj}?u(J?Bjd*P{xTAhvdjNHAt}<45<03Nd6!EaHtL8; zEY63$C7L7;gv%4SOfZc`@Mv-B3F#ytbi%DY?2M3@RY{U+g}O^M8(o6|fsXl9gCjM- z95IbOLY90v1{v1U8ZxDmceJe(tBZ%NR~w4QS@BE|v>YPhXrP|G967{yAimp=Z$msu zp{>%E^UNZQ=K#~BvzLjs%UkeGC9Yok9(SDiKEUA~g&jBuwtRK=ieKn%=DX2t;k55Y z^L8fxyU{xk#oce-Ra?|5^WkVP9lXTddc&VlhxR{%Upd)r&K2PFSso56Lquq&4WHc0 z;QR;&hVO^!G{*wz282g+ETY!;)B9m=_0LUc+S_jk?-Ya)ga#7r+=>onD0`< z^Mh|Q;kFT79oV3tjBAG%kf7bQDlVTpIR5wQTJss=l>#n`fAo8GD68q;s{9WT%bsuE zfH|`j*9j-jmxY{%;XySQjXu$et3Ahhufgsq!b#_mji34>04I>i2C3)Emu;Bgej|z7fdqba7hm1H1+g(*ocOq#Mbie=( zLvA1l+8Rrv7T2*sq|pU}^EI0(Zb^*TH{jjz94=_Z&5qBhQuZu&tEx0#5i&jd%5%1= zGQOF)6`M%D@K$YA@=>VApqL=}=2kTjLG*cb6>7Qhd2>JH{m-kOoom_tJg|D2_r>$d zWo1iWP}AkMWUPY1_XRlizM#(Hn^Rs?{@0bcy9p!YHNL3$y=kxcMfEE)9&{PY5KY~t z`1wR{**0?{sdiH1v1OYYS3-P?@<*IN&2hF6C%8GwD}PB958`V<4qs;gWy#lRD1$4J zAc8$_JmV!5QvOH*?+?AKE@R_^UQs1L!K7Ez1U$X*6?F)LXI{ZaN7}vkc6|TiG;j8H zHIrGN+U|E-?x}}2iQPWUC}>y+%i#oQd{y=1cNsNY8Yv3pP9|(12X)%YbzLl#^XMaKLE+j^qgZ@h}FXX~YOC8poQmv4>g^x*IyhQQo_MQj2ky z>zO-Ly*g^+njPv9d@s+N`KG!6!OL%|Q*fu|pq*;KXfjRgFC5N#5%Q*U)`KX%xQk!v z5D1J8fk;|LZ~0DjFFuYi<}GzNR{FqXG2qIEq0CPqG`^)O)l=SY-ojTEp7K6=OASIE zd?N%Ku9z#Cr^IvKR^!VSD{2V%Mh!o28^Pfc;1>R9xRme_*gM_^8RFvLjc==yhMAl= ze*)hRbD}0ZqVF0T!|)J(T?Qd+0*To9lS|<_@eX@OEo2k$$`n(bVIy33;*k~npov%B z!DL!+a~r3k_R8DTXoe@Zfx|8EF2>^`9`X? z#En7B*r4F{rYjdO5iT(|S__V9l9o{Fj`yl3A--A&#H@8K%d z1>ThRO;68yUyaFFz#R=?@7VWMw{mtfLgN~%!B|qu0N4@16XxM6@9pVg*32K{nAZi6xx5 z_ufji1EcT1YsVjeR69PC^v?SLU;2Zu6uVO(W&DogSY9Eo;P?LUff_1b(*#s>k{UlE z7R)wy)^63k{}WU)nAJF-!Kx(!3e7=x5kzs z*oHJs3zZS4fy}bZS?#u%Z;{Bdlfd%0SGWfsXnEXwXpb7gugpNb6OG~8jUVs92VB7H z;-9E-2+sIK&9JXh2hLN&;xKQ);=Ff0Q89JMM)fxpR9&vbgfSaG0>BqOvf~x)#Rs99 zymR-e`3TTpJb=M-Z_(#U4fXSJ`0&{5Wk%#6=Z{k1H zBOQpz!RL3vU#PM8tW*6LP%7T=uKofn?s@N?FVvVEtOG-fWve;h24AV}UU=es%iLA*rESai_~1N<)5m$sKGQ0NocNT2thP1^7L0~BN~6~D>S~$d*ds0 zyIi-8x*Ms5BV2A2ZogUVvh|z2yQG>Vj!OghknB}hw}Y}uTa z(SPFGPM@u0+favZ8mNcLSADy1wi=fM>exXj5biW$0kH$&x@ozdrkZa0P{;J%>%APM zFGDNWDIHgT^&V2X`$?n?1+VVl++N|HPo9MnSx<4|x2 zTYynUj*8`Gz`&4XCXR=KZj`Yw8e|SbeQHFZ-oE+-F(Lzii?&kQ6Lb!&++qy=9Mq>k zdvZehP^di(A-y5{{($T3Cw_nt2Q=$B5t^ z95T=%V8J=~uC;Zyj^Rs&CZZk7L_!W!Ac;D9A$IwI+g<|c;gv;oANy|YgcsPsBTM~4 z=>4N0JhXcs_S9OBc(bFraL5{6&+gdaqks*#a~^&YOi8YM3^`571e{>j{hCWK)_vYR zQQa%^7IA!{D({7;9;xp3qM5qSF^^~mt_}!#8y~t@U?B-*B;>3{J@P{h50YVloCpF5 z^ErR;JdRG|58y&?a+>+3*}EiDmnQB8a5jiNbR;+`=o!_*kIKk{d=p9rD|ipMv%*wo zhxw6feN;ygQx-+wm}Yg_Q5O(67T^_vSgd3W!e72`ygXTt*DXuupU?pWutlD@PxxuHCpGM-Ndw*4_!JV78cO zDwxW78&6=kU+4WgSNH2;zVPmBGTEHXcY2@X>e8aiIYJwHsq)?Ctmtw(vaxB*4ZOhYT-Y@g?mwC%j)|XM*xyidFU;peR|KsL*oU;<` zU|3+9Ni9GT*L7K$FAJz3@R^zPSh|!2ZO8xYx&F>)&%LuOu&G5Gn=O5f;@2p#sLA22 z0GbiV*C-L%97C{!W4r1Lq47hoVyq0+exuzdM)u52etw-NEUxgr?W&I$POijL9`rw% z?Kz+wqel7AguVlf4tSr|VqIv*?cSpWdgj1nrRg6x^NX5~o27lgogX(F7~j1Lh{xB$ zaKV}A9VDNBq$mF5-B75z;X7h?7wXA+%Uj;wLVaX>Hz-LUO)Itsd^A_LVU)qQ!Ti2D!rDQd!Ujn-Si;U?5!&PuRs`Gq7PA9S-M*mmr=(+b=q~Ox2!}T zmIexE$-f6ht?(*)Xu6u*fAh7h25)4kE;Zf{O4SUj!JAd8WBmRI=zo)v`$+IT8R#R> zA@H4D<^8HuA7Z>JP$DvXzu!RQfboPf_#zu~?Jd<8nR*3%c#>CdJWR*bCp}DTQ9iG| z>WGoY8vek25ysC%7b1A4(@vhCtnmnOoU{TU4J|%LZj4J7@IIZa3PDt{ z392OOXa0pd`^akO73WQmFSFf&fR3IQC#XL7$_VZcrtc4TQsfqL?Z`X3r#`KRU-VKw z&g0b>P$32IcJH!@>i^=aTE`mBcOWkI9e|54UELuktw1MSBnx!Hg`YVM7yZMq%hBJD zV_NW08rOYbTjJi#Bt9&&mS%b9_S8puD{+l<55F40i+Ex?@bX+7-g@Bm*xtGV{=B>? z>SPSWOTG2!CK%xHD_rW6|M0U}z91oK_@B-41x50+S!o&kk56@Ez-O~2Y3nliHam8x*7ViAklJceU-#8l!RNKOpT1PRz7aH@r$n=q zvvB;bat^-#I8fJk$5iP8^ze)-y%70#RO#nzax?DSG?ooYgdmww0z$6uFVogXn_a&3in^Rw= z#Bx^%iy+Eb%(wjPCVQit+mlJRVPa8P+O!l@gONC!m?LbR=dPPLWaDdtbW6y5+`c!B z7hkG@ePGeB05HQtfihFhp6?7_U|r+w8>TCBaCQx{+Np%z`nXp)T#v>RN^kRINqSab=wZfJybFKd0Gjs2cAHQ6;{p;t)+%t3Lb>_^OnKNf*-s+7> z0m$1&jEFBoELa6tzR9@_*ON4>k4Qzt~zhq&0rMPvI}=Qy!<=z+eXGbi?ZUy;}bc3hGtBDj4A%!0|Q zgrs!}dh8`t&GUW5{C3EKpL}tG{u4=-_Y>3F+Vx!T)CPkYc1zIjT#s`qkWrXgR|fSL zvFaVlb_2wVH!{Ztxp8aGwqu|ZO=yfEdU&F0KNvP!T~T9RqC;r@g{Vv_kJ-9HG^jmPP^K_U?xSUpHggp)E@^mcf5rKfCmYiJL; zf3Wxs;-4BU;?1@6ufdprZKRurh?o=`&(pG?DD2uXS98Ni4vpb@oE{w_MnZXWYKRyd z0nWjCU3DjQbir_~rOcsX5lU4)R3vBE$>|Qr4d9!;!{a+tNvUC|m}8bzP8kM`NH^6` zEQ7u%rD@v3L|i&=-sN}kKo^G#MW{WN`X$qB#!4q_?@JsPKwE)BHq)`2M4qk{N&wBFr&g?9<64a$B?d+Mq^W8} zyGDtdVzHXU;uRBpG?fW@%wcK4;kgt!TD&c`n#FGa-p%#ML2+Zm3ikS2qt}@+VqvKL zQ1+ESfOtpg?wiHK8Map`Kc;<@Tf_Fapm4lARx1o@b&F_=QMKmU0SaJn!~tis^ODjKccr{&|t!u)UQAVLDL zX5+0!)B5lZ$rPR_~Yf;rYzf>!XV6)W^v-fd@{`xU&HZfW_A8&#R2t0P)l|mdCf_MP&2Jv2 z@p0Mt_(25!L6~?Eg^QNV7w7%GWs_*^^&UMjLEM?ru;!o^AZWqD$DvjN3EZ$bfl}52 zJ#$^3Q20bKfvfATrS zi&)F)^FoNkr~IqIK;F?AjYX^v+Rm$>p{;$@PIGcYM<0l=KMQElsNVwo%jvr zaOdd69U?=V`jomY5j~@z*bqvpd+9RXqfwKz?qkCwXu;0W7n8)W;H%ZS+$SR=XZ*c;_PFTaTmC)hQ{0_#(RO)cZr+C0Z(YJ z$x52uIawr+<^x{fTRm#&QAaC|FlN>vSF0yltk-O7{a$=G;E%SLQZp+^QT-&n*P!>_ zWE9{4eKT34@(72CE`~53@UT-$ZKhzlR7wR?L>u#CDxV^{!Z`^j6{x6>(&40|mm^foFad(OvvgozIOQK+E8b3o zx(?G@cSHAefWEj}q_gfz7aO%eVi5CeIEPa5ods@g&;yh?6)VsxlRh0_W{5KARkF>m z#Ws_E%r;|qv{^Szj1Kl{UixO5=*S3LEic=qqaW)8^^WaM|49mR?FWyX~}+&8zXWl5OsEtTY?`pV23o4_>0%}l>V-}zI zO?2_3j63-8@O>HAX@!`|1s>mJWTfc=>02x@!{%EoQ6MOVb%C^b){kcK$jmNKIq>a9 z%yK&IRhez{yX8>4nXv9k%@x>{H;YeaX3~ARB9e1c&4US`S={Nme&VQEJo~yh`e!a0 z7V4Q+bHo($hvJ~FY>8JqysMoYHs+9A@x5Ks8Ob$Q%;uE*ctemCfWNR)0ScTCF^g1RS|AdqNf{@y6atIKxc-Fl^VJ-zG%r{ruBWQ`B0c)?&ma|C&_19?nATPYqMzrBw%6}w z&>w-`iVIw-rxh0k9ICY(dc(weg?;!m7X4;{XdQ5j!Dg;%TDCx>UGr7+^MPiYz)Kok ztD%ypN01rsAv-dDdBy@LP?PE&;)8GKLV0A44Phe>0 z0S}{D?h=uQI3Fz$>7f-DQH5^Sp>H9s{!C#@vF_YP{g+}%e%2XVUerAPOuz*yCvB&G(4Q%_bB8u#&AqRqV%;VZLeoG~R+F13kaSjO5 zOC2>?=G68aLU0$W4vq@OG&&YHt5}Cre)RN%Vp?>`K?Hi|UC16sD-)MNH#Wk~yIlEk z-aNlN%O&xSQtK4WDSw0G0kwpmrLs6ieg)!uAEz>JaweRhKa8sc#vRu3*tM^40<9t* z)RHajK^>Nh)awe~QdK_d;GjgjSWA)E>BB-Y4A<{F>C`G5%3m(lCfZ3CI?)`Q5{;Ab zkcf+Oi0|O2$~JJ-t10Ipal5E_l@2~6qGKGvBGbhjz_C7U-BFoU%43uHc4+e!(hygX5|; zv0#&oRq*K0a0j)71JzVvAUzdWhfPL2Ztk#|pVpc42YXgF+2-QY}$6PU4FB>x3Ut+(!c*5!| zdoen?G8+KsOVZ>%tH#38Xgyg(G|XdR_GaU>0+bW0(tIv)J`MJ-Jqlh}@y1a{#(y zXC0Y;h|36{YXRY!v{p%{1zHp{$-*&cal`rntdYqjTn(d?T-bY6l!)tMI0xLzq=+d3Z$Q9}w_P`wKKx0ftI@^jBXne)$neHjHUq#{F5{Oy2_?5>01kvG+R%yo`B5XR2}oeVnIG7?)bekQCJcVe*o?RwFgXV8Sp=>YCq zFNXHBJMPYR(MU?d*&ciLJ0A9^QR-mm7Kb#23aQ@)vC@&x2NVLmzd>}4P?J`ue4ry? z-4xzI71VO0xFz)*khrrqvzIV@S{5FBBo>O7dQRdnt=x$9ed+I6ah^<{Y!nfZ6~Bk_ z;IN0?Ko?~0w@IXhvI+|O2DXm8*Cvq^p;TO$Tx;_Ltq7mJNwg0r6_7|czC)Wf!9Zio z4iOZq7g8_~*QGGI6nY^=+e$@bKfQEf;dC`jy>vpc4w=w`VCKzdj8YAeZ5QXnQ+U#S zaH+`b#A{8y<^!EdYq7Dh(1ZIPuRD=CFMP4=)P?1oQ-4j)^p{eo-E-+uDP}>r6th{h zY0mcHsG_x5#Eo2eTvHn+PHMpz1D;mYW=jUE9-(?m!dcSl{86kdd(%O0cj;2?*esIb zu`=9eLfwS9fyttjyB=l=jFC%cHe;VPmjbqkF5`8(aKA(~-K@yfD_Vbi*=Lolf*LL! z&jQ)V$*DfF_mqRjP}PjmFoVC9lwT%diONJ6viai{tf6!1+!m2?V+B%b&)X=hS>NdZ zE&$~Yz~_NV9f0kqAQO2b2CFN@s3<9O>DaC48>O^lD=1n@>$aj~wKeq3HuxT-@TWxk z8+mCD3gfS78^kmg*KKp1N}F}Vz)@XT@fX|PJ8N-#(UdX~(cE5s-(My=!Hk`)tSPfp zM76Z4n1!L#hETET`#~A(*-9yNo3b&4fUr^-g?eohDLt;BZs`WhVcn=`gHuta#{~yq zdYp9trpM_TD%vL6Ak&<1{v0Ot(7q1O}8tbXb#`SCT;-dUdHGcLf(E;s; zK9U4QGOB6X)x7eQ=&P%lK({^(2DgiMz`J+)Up^|dI#u^& zyG$##qnGB=_U)>p9oa6r(U;}o*3>H}bPv0_d?6C^!Odm zsD6AzJ6}E{@=@vO&x%g}C4&;3!7I;-4oOIxJC=_y@wAbWXTM75{W{SzZ#xvE=S`6k&9%vo-`4Ob|BNa$mVdO_TVR-F5S7{D~f<~)X*LWrNc3=io);-#^xd=tWv-B|gi zydK&EMGJ!7-{VyK66o84&jTjCCHi-khM$Q&i(t#653-D*xZ4e-gS`aiObB|o4x*bC zQ#Ai2k#yZJF;Q>E3~0oR<0I%e69rGqE2>@+;{)tyhbiG@(JjDE^Dr$tB!YMdVV_y9D;Nc|rNF#r0VLSGYE4BqgXQ&v8H=CmB{u;G@g z(q*kSb<35!=CmA}g{qc|d0m$^_jRYN-Yu8s(`XH!{f~`iM^KHH*0xNQ@F%PaSfrI< z-n#&O4WHZelWsV9i4*SiRXBEu)@to(z8Kwnd_Re@omC}of6!^Z9!!Vl4hnUITweqR zI?$&F1$LQCx^z&K=oZ30vKEsY-qf|e@aEOT9-kqD}L83$Oxxk}XeZ6!DwjXx-pzeS#EgD65u({pgOk!LJ&*+gSM|*(AOA>JZrGTVP%WYZ!4lzE zk%2?{Rl^$Z5>2QUW815QLsj4QN*5OSxJ;cdkwWhW-|%v`wpwJP?V{d6N8?y62fITC zW7|f*^bU| z5UvKAcogc)Qd)ad+~E_-=L3H_Dy$4OxvJtDX|T@a*E_U!!q#Z2c~=vSX5KMztEl~m z4j&UeW1tX4%VS*TBgo2OL-)NOQPiJwCuMBb3pmD zruyUHt#kC^apYd&<;MwOZSGry`jeE2duBKkt?L_l`XuEev?oHrf}T)}x=g1-CwY~@ z%VIqu^L{xzp!C59%<#_9S2dW`+SZL)*%5mJ`|XYj#)-&_X+-3eGt?)IfPCwfI!xtJkoga@C)|)oiQ=f0yq``bEO^d%XXEk;& z#p~7!9#9FmAJ6E}F4{cd3mc5l67A167^Y>|yEYi2rPgj6jN_@Zuu422b3%&s_Fr|< z$CchtSgUNfuNFo%pVO{C!ziWtb2|TLCDtNOijko;=g>^?R>eQiG)F7vo)kG|ViP~s z%{1|#`cvX)Y7;NLh-W2g@WUpKS6eb!T36Ope)bovN1J9rejmfw;eD$47{<@Jm4i;h zgtvJW3W2Ss+|R@pkyU@^YH+{Bb9R1kw*6i2>x7k~{k`^eDvRnwoZzj3U4IqPp#_L% zC0iFbsiSn_uVOfIi9e&{SN}6=U3}XaF|5C5Jp^uTW*y>s^Tr`|0U(4KI>r!()iw!J z&@uYtjOgC9MwLlt#rx)f24J(A2t%IYQ;!^G0gm%~w%B(o7WN2Z43;d6(I}Z;iry(~ zr6#obTqY~93wQ^Mhk7jF)%WYP`b%+wD}C4BM0}iG>ALe=X)d>fovVVXE1&$E=+&Gt zYrYcOakl^7uf?}804}a8XixR$gg=#jBeq8Uve&JD=penc7)Fb>83bTeX7o*^00%#4 zGl<>wqI<;Y-*G56R}0--@BNOxuNR3!3K7k#wG>gExdCGEn72oz%wwL;!x{4|)9o4c z?zkvA_B!A}v|~2K_h4gv!t{e0iHG+?57FxvMHU9|e_n(az>eQy@pFzQek&5hX$8hn z@wd=wo}-t)#opN7-E{g}EN)6GfBqH+QkZxH8eq410hL68>J;Xt2J|%?XEsOZLzf25 z4&#^x^xXELawsS41Lxcr>g2g_!my8|Kk26i(JTEK!AHyu`-pi8GzdBkXcBVmBgx3u z^+&q%U!v_WtmZulp=02bGT0QlJ{G&VZrp~?N-?&o%7qTmz~(aB!C`I32cJ0^+gVRX z|0Ucbu=fly^I3K$QP{chA$C4Jg+thAZhYuCZd^lce@pxPv@#NM;r##{b*3KQiB5?) zVc*=TFf7h=%~3qN!Uf-njOM_>@`(QUofu&?yl_yN2dCgX0CsA?EYN;18No{!yzF!N zUi3-!Mz zzk*vW{IX@XCMBNy!tM%aDQ`>>aX?@K;KM{PW3q7}eAEGTwmI9N$<_y~AV;tj8iIC1 zY=CHcAe93n&kBAU1bIXECg#1u4d2*R#wJ#^MEBYqY9doPrHxCv_YX z2w?s|>c^3~b85c+l873Ba$~)RR!?DDABorHK#V5U04H6&7BEs+O=UleR)PM=fHjh< zY3dK6OA=pxU?0=xFLXd`D(YefG149&b@M^u5rikl;5=L;go@%5QD6|GjwYpn3Ity@!Qe(UO_Gg`ESM;E23SM^nP&W3gqy4B zzJH6J6S-R4fT)%}r>mGcoBv|r>?zp@$b9=Yh7^==L?{H1>Qo?&-Orp}0t58|Ki4x9 zcts4&w{M+D*8B1Mxb_13MZSX#v@77wmk|Z61k;7w*;pX?Sv$G<1qB7% z7OD`tu@wtKw-*N9(rRH{(ck=zQVTbp8v!z82tV~=SeFpy4&pD4!S}TrjNig=WAs-I z1&!*f_#TPb82X&1a~ngI)GA#!S;n>CJ6r6c;7HoKRWi+% zfa_k7NijU$u{ENix1ucfJZt^BuVh@l61CuL#YcYbAVo#zo|GUQ0CRHWmTuDexRw?v zdtE#J{fGrgG4mZ`s{ZSpheueo}otWjGqZ*xsG|W5^jwZ$> z+e#Ml^)?$l=vZU={Zb^--9pBpv6l&%p$Us_T*0*dIf;vXZB{D+BdURvQP(rT!y5za zdP2$=D6vD>^`Pxwpwte@=MN9w9tO(oAa*@?dl}eihp_8Gt74$S4q?}WR?WZxJA_>i zS`7n-IHVuS%^x1TW1b*{oMM1|xO%nTKpg|MbWzAiIxl2s{#o`oZKKvRz&;%Bf;Vu9 z0rugL25+F?MTD>qhg{(h2Uy7=?8702-arup?9+n^rR+)75+x~R&l2_o`W^<#ZGc^i zgE?d`13PViT@P9n0~K}%yB@S^1`gOE?0V2@7&ycst&lc-?fYe zEOrr4=qFd*Sjg;m#SXCh;Z>xm?zXlt_6T^@#Z$O5TkGle<}y8xW!Eoz@qh2c!I%JR zQK9Cqn9zqJP4033L$BtYRXzjgmchrc(VEXfgJlqAGyESN_60RGA`5*&$+Jw55M;oX zu0QbbWltjb6?3eM{4BY^lGqzkvjsI1;Dj;izr;tSOe*x3t;`Ly$zP`B>+6_MZsfs} zS4%Fn=dS{nV6jNHV`2oy9E*h^Fx`TsHLRaBh>RQO8hVc6NGFV_YUtvc?LqdsT~wE=a+y0A{=Mg5~%YJcKh7f!Y|T>HT2YT~zF*zz`Wf6L**yU;|$F zF=L>X*JKO~!?Q6KUyU)Ks`FtCHOWU)qGev9$3h_K{~J$$#-_s+DjF-tb&))rVKQfE zc0i@B!5M57f>vY(YswkJbw~1Wh8LnQXJGONdLjYE1_E{WVgs!DXhY~Vc^tLpG1H6F z>`ue9VH1a|uwAg4lW`|7M}zNe5rlKadIsQ`3P{VINYa27vRy~J=dexMe~cn51FW;2 zidx9lDRu-*SoFkN&1p^VIV~LpUPnC_PREv32$VgBYm>-js&nr`A)UfPS77a=urL{& zwNdCXoCk8oz#NsklN*a>M2-y$=8o0K|E5|r-X5m-|GqHU183k_Sg_ztt3iuvl~l8G4`$-FH}1z|Z{{}eE0S*#Vd%AE zsG|5}2|}%fV7AVxWP2N+d)*7vF%!$UM~8*2q#F9VrHrBYmasT%5c}Ie;UR5Kv)~t14}7Zkd@=x z^#L7;mR;lDSOb5^Pe^2R+i+Wp!d>9?H53&iZ%=4I#gGuIFtJ)!>=Yb}7C{d>{Q|9y zk(sH-Ryvd^RbNdhQwu-vQeBMfVg8GvTgy(Z|A{){=00c};kdEYE&1G|iM{4YtuYwo zQYoITQ#c=8W{%kVtowE zfUNgdQD&^{jLh$hl^x7~(yCaQiW34a#!7wtG%N40bOBrNz|F>URvlg%Yfz=cjfeHE zWeo8B;$$Brk{u^ALmM{(Ah0VRjFVGM+#=1w!tIBFRW>;}aw%SpY^UhR2Wmk_$ceL{ ziq#DamyWYOD6dt8KR8G-J3(HbrBXtrPykQ`8}w=or@EB4siSjQAw(Ju44h1mX=2}F zPbSJj@%9>8kto}WeQRh}qHKfJ{)t36waa>Bina|mAiucHW2{O~`^W3{*B1wYDWFkq zxd6vXs_=-5s9r%A-7-3wkJo^QR7Xd<>B}!FbYL3}RQ=fMXcewNqP$1pcr>tZI!X)L zfuYK%C|UM24=YGitfeQCWil0|$SB9JwuxWmvnlc&(>z-_KUIEc;*iadc3=rR`pV+Q z=-v4|Mq6;X+rE_|1uFzd5LURN_IvD4*Sq@4#_sSnU6&{_O?HQU$M7^c9u+Q2lab9o z5l|6G8l5IvQB9h>-u6#X{z^m_U|dBz=+jJC$~s_WcDlS*z%FlJ2N{1opWk6QEwm(u zvHNP>?F17M`#o4zgOhbn4aNJF+}Tm6~@{pVm7aT zf+<2LyUGJ%&l+0SO`gPF)Z8qY1m`jB?8-v_SV8Y)$z(Y7SuzbyOn2pEcb74t?-il0 z@zw=6;Fz55@;V?;chqzR?bmo`x;v6sv@zo>lhdU=D1I}Z-_mR9l=Wal&lTy^b33vdn71FQ~e zw&^45#K(fRO%pc+d!bj3?JGa@16#JYAkga9RgSgf?@UpPo1_NH)42HZwys?H}8!Tf427r@;<@AsbV0jNES|Bdc`+#m7(iCIs5IH^88v}i1?av`gw#Ln{?_KAYi(O`8+R!)k$}0oy2x6 zVq&9s!AKe3yh48$kCeqRf87iQ>14gS1EqhXfz@0iTZ~Ckm9Fc#E45OLLXb%78aQ)y0 z(alq2T5>u-j@BJ6r(pNb$DX1dQWrQJxw*sj&7mozi+VB~;T(HTV+>2-CEX;9F!7p!~d-|-L zInz)+2hX5c(`B2^=|MV~o^ZKf)0WJiGi&OU#W^~HdoujSAq>Pe-lBceWn9p`Hh3nG zKAkRGwJingLh!7-rBmk2n&uuqD{pa+EO%O(yR*B(4m(7xXUMp~Q+Cut)Nh7-EV#1~ zuVNa;L-f@Qc~{_@!7if%lB%KcGi9HC4tNq?qw%{z5!*1F#<*;^k%x+Q<1~XCMtB=F ziN2kQ$<*Z#x_1_qoH0bRFmm*w{j=nLaVV7Toh>hmiWc;Bj!X=U57VURNSktHo6x>C zI09}6&7Cfjy3Dk_?t%NCh*g3xCRWEVdUd*t4c%+QI+^e#ay%}kz#gYzoDQSCbL0wh zG7Xq3yZNsVabXVm6x}yhwvW9tT(jFPaJe;CBaqqevk*EkSN2XWgg=+yGx#%KJr0<; zVTgW&ybIE~4}E0ZxTHFqY`19>3CAljM-DKe9eFjPe6;fRWsvfD=_1=`tb^d3@fKC5vayTR7|foM~lr ze!d*gVlc|@hwl44y6?pp)qQE)0-0{Q>A?kZD~g=3Q0@Le8jc=}&H|!mLgA-2Um&!hYonl=^Cq}0YOJy`tFdk{yZemrt0*)cyh4Ex#Bcw<9D7N%n;64KwW>I2hsWz2Lt9 zPV$03^MU^gnA_fof6gcTybjOzAYAl8Xz)qkvQPMrKH*X{El_j7-USHp2@g}@`Hdxv z@6U?!Rr-1CNVo7 zl*`&bo$NBkd4-lE)Vn&n0ju0;>@uh}im7;+d<5MgE+5cpx+7mEscvA<=hJ0GzcFp} zXgEG^(USS|=Pg{EGtIqVNzTIi+>7s7p8v^Zz3J7?j- zc?(z4@qEa|ayp+c+XPhsGa8sD+Ys)DNDR6Fm>Ujoc7fbz_N9*toWb93)`O9U8ppk7|3TJO>W%+VUs<1}c|2xblil|MYoQl*QDU?IudAtx3x}3f# zlml8HN_QE(kVa9O%a{x|0JPwyETW+g%MpR8?Og^mGypd}EXTIL7of$UjoshDwqb5) z9XomjG#w4?DSL&SZbs7f6*42Ru!GA;Kk*J9 ziynAHKG0=%N0&-z(5$7irscTrzR!Js&cb=U-N)e1G#I@!XW{fY^X}>GHjJ|!DRm{r zzDo#Wa)#09l`^SqYA4+eVQ|rd49t-hyQj}vk~a;)9O8-pjiln0G9zIoP*{B5Xisov z=G^OEw0PmHycxaS>*=$V5;Jw6!~>-UF3;;6)PYkLoNU&V?A7lb5q_)>Pf> zC}jYRSOv1XDR&iWnO*tFDml+=`EnPR;ScP6aGS$@qYDK*Djy28GF*y9M^o{mu+J+1 z$P)K)xLok9w!0fHM}H-Q+^c2p&LNpDqcz|rnFG#*KXdp%9<+6lAoU+vEz{cGgCM5P zB_zenv>PSaiQjYhJ&WJd_?6KIKoxs4E3d4UOQkr~jTRTl4(yf{$y-8Cb#oar5O)K9 zEeJE9f%MsC*_=if$!NOs2^o-5ljSn*Mkot%mJAO3Aih%+Oe>#|iOr^UN4=Y-5E-&4 zXJN+NdDD1k{4GVS#mH3;v<}elT%g<5$_FuF{t&+TBYWr(gp0v*A_sm2-)=9s65qXj z;I>}y6?|v-z})msdOza3hYy@=82KKA2y_Fkr4u322j(HnJ3JIHi!%qGVQy{*4)MVc z^MS(^Of5=etJY_Gx{OG#xE&D2GG!$7E0G!D?p``L3oaL-A3azC1+{lps;T)O9O5-JBaV zUIv|*BeUq@R`k~*3NMp00(%Y8)0cQ!UWPf+4*IH0-Vy6RSZ804Q10^|z~$Q14ywF+ z8z#f%6nf$**=N+;AuhuW|Ht66cq~9X{@44C#_zodHk8X7!b^s^j5s8=7S-h%me5z_(ACroqr_+A z!1l(Cx*RRxa_?I>C2vNKJ11|Ndsf~wh`vGa=MuK0RnN$dfztpo3*AqzJtK$4tOv;4 zx(hDPKquQJqvU7h1I0xd%`l&Tc^#EKE3b!oj6Qx=b`Jm7aJ`x_M(CvcsP%T)v2`Xu z9*O(I^@BSQE@$jVzu7MPBs;Md!<)tPMz}0{pMuL=;Ya(nV?9$df2D|dH>Ix_qN$v-rP4A;Jj}FY)BTuEeY4`S;z&Y7!VN=At)d&7XyaBSSjyP zg031hy4Vvi#Z{w31YO){q6S>by-APC&n@bHh+_Z9LL{k!FXzx}k!)MkUO>%%v6 zeK#DRb-5tu`dQf2QYeJE!nmp9YNypU)lSci%VeB$GlFawhPf~tS64eTM*;t`E^u9? z%zQVi>zqu<9n!Tr^O1Rny2G5KM(8FKGA^H=7?pxdKA$lVCWd9JnaR}5&dlkWpPjXQ z&qEIfb(J6ujp3W0asTXA z-4(v6y*K?$55w`zr_?__Q?$Xa^ejZ-h{)_Nmc=)f7R{t&h_wd?Z^0X)18~#uD zyYTnnzlQBE&nnfZ9;vM*%5nCZ#fllfkz{a?c`WwvF$n%R~4TITDS?U~PH zew?ZOm-l`8lziwu*mY{nqwa&rS5C{E-Mez!iFL)J>Rnxwi91|-REYA$qq;sed9It+ z^^M6#^7j{(pD^Wk@=K?@s&!s{oj%?jw8z;S;s-w67sU0&dG)o!3LjKqErsKT753dt z;iTfc-XBc)KJM$Eun=sf-+Od5(p{Ck-3Zt*TZq{Y)=&paCHQXLmZcJ$Zat=IGul5C4Zu@dS^Vy<%@bJmGr`zA?qemS@=G2y^ z-ZPHc=NL)r{1X7`#`)3ImMGH}F3Rk59yOg|dz3An*!B7OXEe5W#8iv+S)F5B{<-V- z^Xq$~qd(z#Uwz_4=SsboEc{eHw>AvI){mW=o!YhPgkM$eDuj&{cgyS`?g+0ghjVxr zwnnZw49c}j8v^AfmoiG4qA;#$S1w!5DXD1=>&y8GK@f#J#Er}NJd78&mkN4YU#baP zGiof1Td$77xtVBDCV(KwH7OS=S1B&aEb!TyGWllfzIadEu$i@bUw3$50Ax|-CIegF?)N6nl=3!|ZO$Wdi>r97^|%sEG>7-O zQjOBh4iuAiI?+H~@wZ`JRLi&umSd1tH--h5v&I&yEq#3EARytXz!BBbR-q(O0MeOI z>Su~&9;0y!!FndjEetF5q>P}3q-G)0l8LgK(2Njx)1q*)a?K2IVK}QE;F(bo21WCi zHWAxs{7DQB<+ERxAux>UB$S_Q}Ed%B5ZU zIu}BhKDSELYPGLglvx}FDP^w~JqOYFQ``k4`ICd!ku)w0&n9WmWCni7frebqlZ_bq zURYts5F|sc59?xPI{?l&qy>$nBW#}&)FrPpi?&`p$fA{$M~y9wL5*}_(8Ze6xtUjt zKNovSJHhkkf|NfiFUFvMV*aeW7=!+a`4c0-;Dmk$@F;luHHBO*`c9(pzG$>9O54Ay6ZLXsM)ibS2e}9-YF|z z?}~rr>XD!b15Hj-@vnMsKKCx?KG8MpwZC_t=$&-_q;SGru0A{Lle=8<$?>kO_gxnp zo54=_{2OFe{qPOfxMRE8E?S16@V<*8{@!~LHcjti7md?LPrhkBe>0a{%irrSc`u*- z&m|}G(FvD+vUpr-j%3=S+Nh~`T-VPpokPvqRT$VUy)BnL{?7dIiFAAJk1y<+cH7Zim)-IcKsmK?4&A$-9^Tti zxhLeK3;)u8dUVZ$?*ZoxMXS*}?KU&~X0Dyhzpq<=1s{EV{izsid)HqG>dyVh z42NNS%}2jYjpMssOO5w--I#SJ^*)#ED!P+KG<8@cCT|wdLR4h?$Djq_2unXxb0mtA1u4=y{A981oZUY z^3d$ioznHGz8l@6T}=<~BLC}$m4E9u&zLIvCPV?tdUG;xGO;Ue@9O*J@A&NNfAgF1 zuNy&Wuk(cV{392I?)wOO&d;D*l`&ic-{(d$+{KeldCjXVc zUrrBy`1@l+Ba64WJ9_VW@->>(VFb{7?vGbNq+MV72{Yg8e)^e%oAu+F^+~Sxp8ms~ zyP)@bPtOe91zq2J`q-`+&wR>V;K#G`nR_XG_s_pikFWZdyNmnoemE%Jok26Z*v0EU z(RJ%D4(Xcmi;MSr(R=GJe(Actx_d!*SJ?Hd-A8sszkJK#hXZ7I{DhXr;BT9=OaLIk| z&P?x$ejmHXyC(c$QrF7o<{rM_8q7TxfBuK}PB|;itSRM0DKcYYY%=VAI$Ur~*C(Ev zW6s{$jkB%wEIq)l+q-Vtw`%f(4g)4~vdtt^kZHX5?|qN?8t(6^ea)>O?CXcYS#db5 zGkU4NW+5hD7-JAMb-m{KQd)Zu(mDAx25Cn}NAWkRtN4fale_-;v%7xr zhxfrl*FS$H6Q2C^{h1?@KR8z)U;BZ~jIB}VJ`MpT+q3TNZeFq~=cXia&K>IW5Ao)( zyt~HDPuAt#Ij$x7X`TQ@OY-}?Tj9>%dS=1>dyb}#Z*W+ETi@Q`{vXt3z0^Fj9`O?v zeLu<3J86Wz6LUGmh4?)~nzN} zZzMT>k-L@T%ZuDQ-ECW27P}*yYfa8S2{hce_0E&rH^b>)bnvv332%;s9GVDsx!9Ae zIn{mH%}tIvjj8=s@_6hfCAXaB*1Jz8<4~isk^FZI&3-GHb*7u0 zuW1Q_;+e?@+g!UZYIbKPr8C@>qds`xjA0*aU3#XwJOt)@ml@2zTIOcD9Z6xidp*@I zUG7S5X>#jwcL`7Z%kAAh6@Du@bcGWHXRdHtW`4^_97Or%a7}rflAEGDVFC=H9m%A# z-0S%Kr)Rn8G<@4x)^PfSOw+qpzslZS@hW%t!oRC^S;@g}Vik1=!aDCz;TPgU#z85I z;L6{FcB|2Ro2>z@B#*qxU0#t;tBs#`*T)W56bC;i2B?#W>vOb36WdAkyiF-Vi#pjM z#eSZMOW=T$oLQ<@vZ7S0?o_`?oSP{!N?bOHFQ#ASb6t!{X)WIR!M*@bPpw9oxna$s zj5>@L5Li$Lh=*$B`Mz?q5>Q4*v&xN1qNpAdEgRPms`!qcYTAc!T#o5>xm8h;I_+S`^QaM0=O*x}v^~q(d++ac$ z1%VAB3#6u$i_PsNqr#dQ#0P#GQs(NID=Kw3sJ0wSON`C&fM#MQIK<_;5@m1m1;DGmf)?-P?Rf;Gi6TJK{&xRL;`d1V?o!$ADdZ8d!-noL|s8= zI0z1Ajd}n(b2F_zib9TnOOr1nNK^E=6^k-8 z(Kri93Y@rB;5g{kgOhyv95<~pP91B=3;2da&Zj*$@!V(+S#nv+)h~@2mfoOo)>l8P zQKkA6N6MBlo>W7o6nN*a{}c&CXl8s$fOlXViVKsIUgP=@Sv}{u$w}i%?1%3pEh`;1 zda`n*Yje#>*GhK_PZQ5Yv^6JJor}EtXwr8s8m~Ed{#@7ND<3x%9!F5#48A2VOEA88 zJlT657RHgu39ohUrj?%8(#nxZ{(N^nt-SGk^y!gX?>yg43EhXbe(?hL7tVbssa)h{ zZe4L9%7X_AXC}A4!9B+ZytvUF1MfJ-M|Z@Yz~*9E19<)-DzLo%p1z=s2~`vpk@`R~ z`(pPkfc)2s-A7Qz?|zf}&+%J6*B8tUf<@FX#9#f~)|)SJ6P&vz`Ou~AJd&p`WyRu@ zQSHZec*vUc;xSuZH_l3Q86 zhK5u(*Vg==HVi`k^*tZl#`EI$tmSW0TH3!vWi|e3;IP})u3e?)PSx2vUhkTe-&eRz zwVKKQA!Ds{u3fuUk-xkowL}0@QI9=MQlzk18Z33IC_2F#8b?U+bbayo)yGn{n20e6 zp%5tE2#2Q{&>i{*#HtaID&3JuR$XS(`qX9aRAzVjo88qUx4+rlOcK7uwKp+Y@K=_p z`0A(og61Gt7}W6aK(h8N?gBpg>08{N>m%4C&lG-(kB$j~Q}}3H{9tnJ<@V7dm%CSC z1dhLg^(@lgT!h39s|W4Z zj%<508z<_bt=IjzyWhFh$*h~*Pu=RRdvA8{4#PV(CpT8yuZ{yNf%rK7Au$-E)~P)F zp!$H2i65;#)bQ|)&B;eQTs&R%%wLP2<~8c*2N1GVLMN|`S@RkUgzU~ z+Pw9&zjPmS?(@mt-{wluBUm93TK5`~y!2}}-T{AQr8V1v; zg5rON%rfRsQ2ZO^c1kk%Y(Lr1XZxUrao9)Y`ASw4{ghSofReUV5v+{4nH?-*p)D=n zPyQyK?IYWf7VjdzS`~MOr6#3MgrylucZa2!O811NS!!z_EG-3~^;ia!o?l+1z&kYrJy2~GbStDT(vTosIkQ?NPCKZrB>4Qk%oRe0h94MP^^NC3qyyAhU6D*vH)R6Ip0G#yy)k zB^ZYpJ6_VeKCY3zsh2D-l9)YEAHiKUJW>C@GD+VD!l}svA9Ej?R)FK&_10hygUjAf zs=i5SP7AQz}!Cg!*)CsMBt4I3&T4oifWBevswmQr)LPaGb@v?N&&LQ2kR#1+=Uh@B=5S*&8)Q0 zB2?0sR|p}CG}0`cB?R{n!;C}tO%IP{v64u4WJ?p2K9Vi*`CQG$5{`DTEc0 z(?9M?bvY>lZyzS@A9wLAAu6jTHJBA6SQ;t%#|BU2-Hf{-*|E_z zUnq8ZE?C|$5%MkAbj9oSHf4eJRVcE&6}7emAITuoXtL3lq)lo@8j4TCH!wf5kSy5b zrqvlM^kiYeLel=%?(Et&l#Y!Mmi}vWdm+i)O>D&z>S3N-Ad_cO65J~?fh3Jwmn0Y8 zO#r4bx$kZ_a~wW|5o$i!dAGYgb6)+Vq;#k2O!`0JE;@P?dCnL{Vk5|Nx@Iqv+_Ke8 zI{_+%8gaA6q{jMbd}^#3plmmO09KWmv*jLlM798)lW+97!;|on?$D_X#j}Nl9$1pq zEM+0Pnq(pGGA;FmWaqctjEWZD(S%^eQ**;0HOpOYFe-+c8&w@@Hdz8SL$za4GY(fB z9uN?wlLtwg?0TQ*g<0zyBIyD|J5ki1w{145IBH44Pq`(Bf`vysY}&$6H%P@e)e>=c3VbQIobyZ&P~SRRv{;o_`!31U=X7PtWIVa_0=ub6s-pCb#q) z*s0{|8$FG;-Xn+7<~QE+eki7_L(r!5ve@9d!rA9g~N+qb`0{V;zG#Fli2L9YyF>v26qnp#!psZ5Y4^Jx=+@#JR+F za@l4AKtuWfh8i(?I~03518mbqIt$4an;_FZao<0H(`tk0p3iOo04M&hG{n#J#0 zb)UGrG3mbF9a+N2(EP_)nMxERElhP{QkylGPj=t$4jb2cdbk@+0=*T#;J%Jnc# z3xCYb^rWWRFr{a_|BRV(_rw2uVRm7%q1P>LsItyD>CNP4z3z}n z1>0oT5`0HatT*_QhAr+m*bKDEs>4z(coiof#^GAprq-&RG_iO>WKr(G#!sbAsogA5 zcX&rqvVfw5G87RY!&sM<$E(5Cdt?Ds)!{EhI8&rMO(`S~ZNX%$P5xmED`oSNty^3) zkH9{7vE7dp23~EL+?@714=2|L)k{<(b0R;vS&}DaFZTT(;e!7 z5BE`apUU9c&a`Z8VNtk6Wy|D;kuGTqVB6>`?rKclTA94>O{Q#hryM!TJFXlZCz-6ki&<2UP51L(Eeb5lK}u{j2V*l_Og74(9<|&B<4wHV9}?I?UrAGFCEQ zb61|Z#P3#xSwY@HecQvvC&p!ec%%h5ZBIbkGPfmE)9YJ(XpE30{M(oBiQ^gMQkCieHjYiT{ z0g$4&B?^~DnQF}18|~wZ$ERGkUlX3g%5+CqNI03gFhqD>gnUR*&wgyfx=}e=2pZ4P z0osb5JT3cWnHqF|V=2RXBi9L|e2J7MT9V_x=}IWo6b-Q%e(yZQz9<=pDvr2W78Ww$R305<*s^Qb}aX05^FEfNkri zE+Y^~sZ@O3LAVf8A8{8fs8i1b;lz<4CSpr)VTw|m3ByxM@CMlvl$L_9lHB^)aC#aE z&imk4{s56+xN>wPn4&r#B-7G{`c{wU`hl-h$3t=8ytj+0abWebyrBUDDySbvggtB9 zMv~LM<&LbRZE8a6hN5Iuy;0Rh<^(hS=4fDGX$wmrWU&q^AGS7r@;%6woXH%Nw3j)x zynakFXISjG!B@zq61!N`G&YVTY%`1_#||FPNd9JrTlT_Hw!^>U-oP<9 zw8KcEesdqpwAK=t02Iz_c`Vh#I=I6s*`P}y+bC^rfbA`{DR*?_73*vc_vM5~4npM= z-sI3zPOQiwsN8V(qr#{nhyM|K!Kp{wyJ9@b|LCALG&Icrh?pmnD`7cxWq>07!dln~ zhuilg7O>6%#`F}x7!Uvx$b(t(XZJvq8F4DolKTH9Hq8vLZ&X2jGeD_lCL--9Mia5y zr4Jb(uL9~QB)5Lom1oe_Tw<}!Qi+>ROX;CIdQ`Qdln2P*a=uHy(8FWJ{5 zw+GQMSckFNt6Kkx$+NY*dO8cV5Yf8Gmf0E?2J?Ah6{DGC!opw=l*Q&5U!YU z&#MDDt%+(O2eCMpiI#`nklg+~SLVQDR?D1E8&julk|Vz9eIHqMgLA-0;l#; zBzJ;bJ+B4Xk{e4D2-G}khNx~{%s|d2ZEgm7i)BJHC9N`{las&iPC9z1U)lO*m3*ed zNMq;?O{YyJpZva?a@26>)&D-w1l0&h9x1jq1^~wk{^JfYiUL@p5P+3j=dD+_$uU20 zQ-}pD{ehcP0j+&-3;K0}->am_Mqin}&ByK?=vz453K`0de7V&-6Hr$CiwR^Plw&z(H8TAF2@mmN;5-z#j$a$C)Eyg`4 zf@voQ-(`(hYspQx8kTJdCZ8PaupO`THt7 zJeEFeA^(KsF|KIzDa+f|4IGji%E2+(uk{iCHRJa*q9)f;bCb&1gai$DrHzPUx|OG~ zJCs6}+99GDhj6rWq@oQS?CpRQ{nEXE+bIN#W@aG4MW9UxDm*%iB9*`@4A zQ95g3xKSxi>;}^KS!5$pTJ*WYvZCMkKFf+~qj41^ttQYO;(otzRa;A>J4{fJ8nsVm zp-bghrn{AvSO8OcI3aDNWe}!xHlb}&u^UvzUPV%Ec-zcEXq_J<#t@!M8*3$PIODB6 zXv4Plu{82O+M_g;y)51AWy#G+j(svSDH(VIQq>t?Y`m)*3q%0&nQV@AlAN~5)-Mbf zC^t8R*PF@Z=Z0*&8Nl3AmOb6O(`>+;Wof&^pQpzP(?tl{2VAaIk}WqB?{(gO8~Uq0 zI$TAXiSus|2j|eW(V7P$MXpSg-yc8t??JXdo4}Z`%T+3&O`$1@H${dSt4xh=gcbJsX0Xth`i)A+swuhFtAoPx-w1((nYml8(|mG z$R=+dfgBv6pX}vYLvX*I=w+Q!7T(}A&dk6GN}=8kr7>E=MXdM|^3xEDPwR=dfs7yn zv|C+;7yE||j*i0f{lj|o6IM9vuK{(sGNHROc>pq4*rpA(wpr{{N)Ve)$Lvf?`D%pr zsl7vA^F#*>tB)t462k|yu*F<&t9`hIWj{hc5~B_}sdnx=OBU=Rluna2$KYVa@ZNUm zbL&C9F;8jN5Z_L{lJ7UNOOHDc0q$&g40Ft3DXJjdh3pL%wy~BY zaWeJ(x#&r8G--1B$iHu{zSp+&CDil#(qXG+3cQ)mLw}YhZ{P7Wpv5oJXS3RYiY@7N zYTYQED%VDX(;jrfJQ#zo9JFd}G_*i8zxdna!#{B|2}%ya7aRya`(}?umcoT##IV%f znw)|q!lLkOQWh1oa!|@H_Bo{N3NK_|g1jwyft&FHx2AY+vihfPa)r)1boy0v`F|i| zD<))9GDDo!aJUByB4g1VBV=s-kc>5P2vwVmHQ5ULBHE1qtb9nu=GmAl8B3IcSX@bd zD(#-?uX4#j`ipU5J(V{`wR+bY4eQFpc`75bS1e-BKgU~|!<7re`BXIqm#*Y0kl~WMR!Rn_s?7iL zc;B%54m%W(OEKP=|5 z*=DGO7g)Chlu8x$;TSlc_+O&(q4g2U2l!HW4yb&FK><4IDxz+kmc@a=n>E76n7~ON z!*FrZ1yIxnMBimB$< z1z8SS=lynVR5L9|{-bd?8mn|cYMsxIh*4IpbK!y20#wP5fxJ1a(3IV~WLGimb|oAO zHQ}#_qUm})(=<0y8Emq=tCgucA(h`6I2|)CoV!rt+ni$NDJ$(yOS@HCOc{bb3Y`3; z03*SyZjKMg5RUEWi5*$Mho=8KoYyoP{6Q;9Hl-z-(-M8`b&F1R*!Zt17mKndSx$Ft z;dul*Xqeq&!-wBr3Cr~w0=n1c&L(`b92ADI!yFX`W~F$@R@z5u4Gl*{f9PEsX23+G zh6}x~S1OtyAk^XWna!EhA<g*_sjsuWwK!0=ycfXF)>As0?* z5V&iP_h_Z4w)lvPoR&kgB!t9xiS5PgiE5ZUr!z_A3B{k`(%AangyKK5-6u@m_H)M_ zah&0m=S>vNBX`xBqj4DRsxJct4RgMA}>vPN)E5@psquksKmd-52yBur{ z&(hN6hJ8VMvG|>m6&i@Y6GZhJur=iVTYW_4>I$_v0r{ajz}->gS)VPQq8d%*24iz- z$;+1Q#1Vri8<`0l;k{Enr}(It!TT1W4&nA&1P+uST`e|aSp+d5A;6j?iEA}2B!g#+ zMj{p?c|=|7tcDi-$yo=vVbhBZt3-}mmA$qOMv%R>kE-(3o>j`hOTt5Ve0r$|`Dz4y ztj058%n-9h@0&{TKP6_WkaW2+tG^V!OK|utkE06ZF!Egssrk%s- zl!;j@-DGr3!ys=+VJ0#MtaoI*DBh4^iHVZpl@JIqG}Vi$$39P2ybd=da@rX1Wr zo$=H5I-(qbJBx(zHBru2lS!U3BtuDY^7c1zP2i{&;t7@j7h&>+o3Lu3bD( zCW}O7a7$U}X}}TRuwa9pb~^=c%z)#m--0)$tgwU!R0A=;TWKn#Z406UEfy+5-&&~X zGBba5RIS1UQVmPfv7YBL@&kc`1q! zfQW=N@QM-rXaK|nYv|rAx-uDLfdUD}nLeiQ3WP$=fREqRLO)@Z3djM|gll^wdJp=A^8B?GLc3)s`%ETQr$D6Genv4{->>g~?T+d%K; zWU3DcZM@yfiHw{D(Dj0*I+fA^+SHn4&^+~|rARKC+K|H)wYS|l6AD%X0brQeKx#-F z8dzTOwYjiO@ym>&CHQi#Y^VHaEzYqat6Z0S@;}|=Sq(h*WVCMeLpxVXgAJ#+_XwF` z^7wzcsaMh_*Z#njv_t&T%rY&+$k7wpvNUWFG9EZ70jX(T30K%mGi5gO9a)=2n&F}t zV$NLSPzrU{FAd8kfA$-9d1gcb}Lxu(kf9i`rF<7cy+qD8h&Ts+& zihb;{VpF{E?#TqS8kz7!SR2#}xhA?aUWco@PZMgUO^(Wi)lj)xHi9UD>bB}0IGY_6$MVZ*;otgY(_K0WgR;Ee8G zvW(FM^|MEG+N~Ni>^t3%K0%4JW{fk~sbPXnhxJ)8dOVm;ZS?H@R8*`ANd!I6F|J{; z{;&~1RXg@Tsbg_%DHf?95|~W!^WN-yDZ{7A++U{>z;-jk(%JRYBCp->7wMdC5xHrQ^Tc z5wE4t52i|8>yaIPIGSX`=^%A=>YWs$`ej^3TtnmW@Wjdg{Fj?K8Gl2p*_p>vAl<=H z_6*Da_#TRo*-vb140;S{|Kb+PD?p8!+_#ldHfR2neHbU zE*dEs(zw0NPTIVxbLSJ=BCVTF#LLj!aXigRJ0x`Y>Ji^vM}b>{l7IXkxBT@wt;M?@ z?wzpC%eBRX8;=dd51wh#s0IXe#cwwe`k=ikLi3s(` z>&X+)dAw)hY)MC>RW&aoHC8C}z=+pIG!3(oZ!caUq_Is9DIJ3z{)#THRO#A+xLQ=e zUs-jO8f>N(fNk<@9W=GrP>MtO&oxo4xuivHz;kRmPA?F_Im^Y{Xv`IVXc16XAvS|W zLlAo1NBV+z5xyZeAw8me_ecAJ;(c0&v=GBuY?%1bPs{%_lb+ojH$&+NcifH`XMGLH zgZ6&4=ZC8(z+k?dL_e~yFkD4B?1B@dD<)Vfk3V_c-Fks1B$q&|9L&cGUXYwg^3aF% z8W%?QjGRh>C9;@u=;;I!9lVq~YyrmkB>20{l#|RM(K%t;K_I(bOxlo~yU(?rvQDlC zc*N3Sl5k#(KsRgB5GNe!vgdC0J!%;lDPn)SnX1<0Z})LidcBLgx`4ZWxKG#TYu-^K#r8|6yeB1`JM9nO`qJEx(dxPS-st_dleW08o6F5}_t6 z={U7-^lr2U8X8urf#_Vro0`1oc~^e{WAIH@$G2gBF*(@5dc8Z?4yGkfKJO-7K>sG_ z{ge-OXNLUH2xP%%LNo$u7~1&Ore2atNU!R2z8la84#|u3i`OTQ?{o9qnvZ=boHpk` zRO&-594((5HGJ$l;i1}lGY8XJ79rVL=1^O9GJ9as(t6ZC&LtLcX_As`X)2!|a4jhC zmK$4=Ox6+PMiH=CtcVcoYp7nPyc@~N=4<(Z=9G-yo>k%3Qkp8dt9N%^A#gIA3RnxI z-34x7IP) zsEA*~0V2SuiN?!E;!jOf;kjz{^WpBi*CIsn3995#Qoa>_CdOJhI&EbJ+c-BFsMO&4 zw&j@g8CX4EIzuV_E=#{G`X1Gd7JhsmR>P`SsKP#E>&%8oRr?B%*`ID87YX{8Pa3No zTZ+m={0C>`#im3TGGctJIV;^`r9>I56eh+^VdsugL~;k0O%ut+gi|Ag7WR_H-PkqU zD~y&SCm(;h*QX^_%(6p4nB~nT!YfKQa~MG0CrH^)Fz>TGlcz!3UV@G2m5qyUJHP`(x-vE765nZiBv;Y zMpx;TCrbRO5Ek4kG|(|~#?ubfuaFUT*wkA<@!RXo1@K2sRd8Yra~-5M$|=L<0e6id zRw;lVTQSNz&~)zy_>hS= z5X}$c$?Zh&wbj+myl4vya-t}{4Pq@C))avP-;sqO**)auW>(4U31kSZR1i#C75T{g zyOn&PPueaN)vn-ygbq};Qbn|@A}ss(1a+OpZAgL=p%nG5eKWL~Je~_DS6-z5w7UyM zV(QJhflg>h3*neLS_p5$NaF*RjzmKW@AF-a)|V{@>IvG5%82!g^db_2n`_}A7Mj%x zl3`62D(AUavqdt*JBUmm1&doT0yPsvg)|fTeI5=_qBb&73(7$0(d^R2;$gvwOL?e_ zjihQBp|M*8k}mBKJMo1*Dy*%RZq6l-=EFIaK7ieke$ij8rovrm;WMaj8o+cnSPZUc zYBZ_%B_l}$M_ofc-h5YI5YLNue-<0`ld?hK6hzh%r5(mGChcMXr=%0kF_sly8@GZT zqcgBtB`=Q-#Ys76p%ptu?W=MrsBc%8wd`J~o~ue#IzW~SLY3W3wx*r~mDVYef?iN% z_JpKhCf2i+(ijLc1^z;kO9b$lK?f$E5Y1z@80`av2?Ht$Xya0wvzHGfyR!|-=qQME zuGxystlqS1y=jUElw*UUc>?JT4S(ZltP2OE*HBL#a;zM@$$wZo^3g8A>$OMo(H`PiVx zk4RDPp=U~75uht0iA9Ik5CLG{?g14Md9oR%VA9#wI}{868{rzcSu9k@W-`_*<=ak= z{7i>ZVp)5^N=iBKe#HmVAnrek#&wg!5VEbSL1Yi`M(^NMz)|;l^0?m3{O^x!mX}@> z*?%0s!ZQ-S_}faTEoH!purNaasy#-}yYr<5N2vpNJo6O}SOM)^lgtrW3sd_-Auv4e)mB*i_!_Ygd*|I{+?*D%hv7OEKc7 za*rHp`cHAvfxs3=Nf_=0KMLfO8+8(h>>m0BEnGR?UOIgcC4;zD^O62E7e60{t=0cj zGxZqMvh+g#INyuZ#cuL(e`Q>I@!n9h*b$W>y3vfQfXUsrCNsmG>rtfZ#AG-{lAX~dRv(r9TE zlN9HA>~!SyLrsSCec*xffHwGr8c1hY0&@takn3N9(o+6kF^H5#O%;D?^e5tys@F7z z<;o~D>`0L`*2fFEycfJS8_$o7+<4DW#dCHFTeMLVr0O$FuphW6q=y?i@>@yih4 zAZ`kf5!L@5JQdJ1hGAmY?|^#6JF)V-Wd(iV6JdIrPc$8}Z@rqC(K1%1C3vvFlI+!@ zQ?!X8XQ*a<#McT>dlEHAfd{@~&DqGg(XPGTkC^StDlwN4{(EJL?yYUtZWT?AC|mZ$ z0Oe9`1GT*0TqnShskMrTW~9Ej$0-v(@)-s3WU9&%<*KwyT-kK1%D9Tj$}n3I!42)~ z>+9j;cp{IWL7Qse$Z_pu#u0HSw)hnluoMtYFTT%)HNb5N^ob<0J-}=gNYNvjZM{em zMThxB`{F$@1H9K*5%Xnd>RHT3Qd4e=C=lc+UvZdF~o;rPx$c+)slvKbo1T zN9iClAE-SxY_>xG+LuCu>@hIr@kYfC;nSEvnz@fB3m$-SW0RQCY=%w1xMl%MV?)Xs z#wtvXSVT5P_efTGdK9ZvFt=Zt%`DzE*;dxPrC@rXcZ~pgr=E;H08jK`j7=pV9t588 ziX!^O`wH<>m@UvJ5y$dxo|$be@}qP>!1FGL+)v@>=U+4Z0$9Rtgs<(Y z8pO+D{B}4*%eQtB2F{(*+J zpm;x{GU%z_2Y)bXXXCw(7t(!Iu}$zuU+nlt3zuqlf-00&LuL3ui*Wm%T+(RyK|O3gAB zjGCEMQS*JUVp?H;Vm7ViCqtrbq(jL3%j11%ENI$cgS4dlQZIHxd-73|Oudkx*Vz{A`uw#}-&Z#gMr2f(kJu`75e6 zDK(jO+R0HptRrECvxD#%=P6vw%%jA(@fsatQTs-#vP5QIRd~syHkse;+S~365lP*G z&o=t&_5(Gu8IeTLOQXlZvV*W~z7vE^TXg36Qom=aWP<}A)^Yhv8$IPnz_aW2PS&Foi*zj?~~wF;JC=5;e1ETCvHp(@%(O7`7jOLhhC`^OY%f3#>ns6kXl2API=Mk$3Le9hu( z1y6qh$EYG&fNBpEAvF%GI(UGE(( z4t-!NViY2;K7-s9B6O>WISE1vdGn#iae883EiJHc_pDOIeB^_5+b$1t)hIGo4IMA{ zEMm7vC#dD4&boG70IHG4*ub^(dHlZArIRNZ#irmT2#5T*!_1G1ij_e}?y%I6>t>fO zAmi)m2#dKz@gP1@J7b3=(%9z7aET-wjpcLzaMe7~>6t&3(}72)$W%K)nuW_TIrDY$ z8mcC;=GFXf^X^j@e|lslEoQ6?SRy|wFkyuwA5s&j5U;Wu+!}L6z;x2A5we<=>(KEx3;CG3ISY%WbFu31(4OZ_+Ut9%Ld?6 zNj&gRwlWy}(-Pz`ONHe&eYwgT9*k|2Npcl0Y-|`LjBU6(#&bB3AuxMrCiO$%oJE~Sw1 z6G}1jcPceUF0+knyqQQTE(q~hjC@5JEr!d~SE|vfc=rM4zF&w8#D}d9ncWii3l9dXr-}3rWM6)USiDj$6mfEl39OoZl_*N>zV*7Y32fZq zPcD;tmS0OXOK`7#J=l}&AbwIyu)7Z08q*ToAegFDYgV${r?lE&>>T^2Y6|}8EI}ag zqp^6T9}SzCWcp0B6^Z@Ub4ZwKZk3I<{GAqZBwqy}kk))>1 zTZe@4xem6vtDfA*hM%G;>~o-&pBZe~o49BBP!>#055>VaJjhUq#mgjT$!ogy*2?C$ zYhw*hgy)y6U8!YXJGj6bo0SS1WX{)jEFvsdx+^3?Mo-JnoteEQYGb)P?ziZ?t(Pmz zFLzk&atH5F)NG_u^4iK@Et1D#mWkj|B7QRS(%awlgs&}DXa=~C6*k6Pf(cCW_l5=T z<=Kp0f`qQBU)4louMaG5Afhnj|D{rK&>yb4FHDs=#H7aRqaETG7jHU;uu*Gec*3R+ z5O=AYYa&i7l)N>U*SpbL-DdvHkCvZ%sI?+|@{y;5#2vzfZN<{2`3 zT7=QuR%iQosGes{vP~P)yIk4o^kP2PD(k&rI=@3)I%0lD#)OB@FJ97E^LxNhFgHF8 zo8HRc!i?rO7`4O{uf&KdTMClYeKnRkyAN2gfS=6*$sS-tRx+RdM3w}bCp;Zw#2jIJsPRs zU8|L7W9f8Y@ebKYW5)0MXnk;`7UPkP^b#W9oOxqP#y0I=nq_a{+IZIi&AFY1h#|+> z+w&Cz7k>I<_CUNmd){Og@~i$pA=J#T%~cn#I4WD?ouRa*g^~S9qP6K8-#`WPg4nVyaQC|ums5|u zM}hvJVE9|3KTUQ6z9Cui!qR^($MXO8q`%(`EOasF zDZ0kqzAz-$`%n zdd*zx(DuGYzr7D9vT9Yad{5;p&Jo`Z8DDHcP4^^Kx8T@rRed{h15WirA9COriI(pz zG(=uP)sThtrKNoI!qE0UPPNJKti0I%zD;9q@8eg)AhP|p_c06^%tPjK{jlwQHtl|U zpJ+iq6TV{ls+$h{F0}3ubG~8toF8Zx8jUgFgPil>Q(p1Ye{jJo9bhtX<>6dE0?tmJ zUW)MY@M8bqpufl-zn7}tfHL08Wd-jRp(*9AS&nQ3ww4t{r%T@Gm^lY~DmvM-?z2O- zht<b{ZAOLG`ZKu=VS2uDt$8A(ObIa)f@Jv$A^ zkvS0d0*=;sEs9J*0_i)p$rnH1BxJH2cT`Na#EoFO-SI64p|r$)rnLw%v>YsFezf&v7$2isTThW-xFm+ z=yzFQs;H2%p0dj*FL1Hz)$xMsG~6sqHz#?R+utk2s~H6?1aZA`R@tx(d=_Vu!N7Ze z>4Dkre(r3#YB;K?xiehTN68spi7*6U)A@O`SuU2Rk2PKU3?Y;f0%HeR+ZA1-v&L$TyJdc4tla0~f=pxsLqFoP zjbZ^jK)xr(l%W@?1S)-}jO4a1nMEGEhk%^x(J` z`C6qV)E|#VFc6NJZ*hK&)Fp6qP(6S=*%!e6NKJV_1-yembJfZmzr1@(?JuaPr7X^~l+@GH!005^RTr;H_C#D5h_&?h>Snp;zzhyYaFLMKcy0Sj6+ z>|1+Un{6B~ur{Y<57MR)$4?7fRc?9$R<<>)df9n)p;rMzQ5(o|5xF@YkjUwwxqL`e zz(|-cThR0gh25r4@=+nxC(X*4I*F*O>nX6g=uL`*&sW7rq5|IllBC;(J}cq2(q|>x zR#1hT#V6@2$u}lM*_)8c{9uK$t0k)~ca^Bua;qfkO@WxkNEL`1R3I1`Tn#^5fpBXy z3IxEUdFjP+&C&>#B2mEZ0R8lVBGVW#kJ{(YlO+*K7~ajTPkBGq4yPafm61 zg4_UrGdG~9dWI2o2u7+ml7S%IMaF_1@~6hjR)ZZViXS#nTce+n;St#Joa{qQe?69% z^@`bGz%xgB9ku~vnxIN1M5Bph1ck8ZXcSU&Knj^`Hbi-%D5OG9%%hPAGrJ#&OjX(Z zh;K(yNQOD`7RSL*2*186Y>q)86^e(E$V7cMibQ-n)uI;x%FmRR6!8og`)`yX|m!^H2 zs>X+XzYQ)#0tfVmb`F*8?z`ypsxG866n zBO4hxB46=E2ejU|Q4xmJ)5{)F_efwwyf0xu6+fb$Y9rN43%$Y^b!*`fV%!m@powN! zjFfBhi(7C|^~j1c*Bi>*9tx1EVm6Xc@wc&TA!A<>xk5Rp7}<6Q;mtPg{9mh-4Gr4w zZo;mQhjl9K&_Bm@aJ*3s-LsP(`J}z>q4>%M;wTncW>E~2-iJoijdlanasepTv3^6L zQep^$+pchXY>)k(aqZfOz$;z*uWeQFGu_&?Vmt9kcdK8%ct$&XPfB;(*ZN&oywMgb zLJ->X%ae8`5Q#8wIHOtCjSzW+@om6^{!=uReMtqsu&}pKnu1Da8;&&kh{%uKJ&Ac- zmGmCntRvLi-^Qo+{e?b1r(81=*ME<=MatT@H8TWyY>0oQbm?ueRF{7EGf;xH-+KKc^>;s6g5MPi$__1!g1ZHQhc!P$yBgb53 z_yxtsaw37HdeWiB;-8bU@45KrlA?siiZ_#vpsqNmGbwB&W{VP|XpHNH0Ox2$0$Pl| zMiJdGnrl*ownXPczf`SIeM69}e?xd)2DY+aBuM&i4C^Y~8#(N^2)LA}L57Om&KlYU zn>*55HHowJ4PCIwm0ou3W|mkL*XyDZ5t$^q434CRYm!LnOWeFnXs%SNgm9<7Vp9?$ zC?h0THOrP~4!cTI_qFzCg*Y-?r*C)K-;rLgye@~>q`WhScp_b!^Vc(XM%lsL3SHAzXq_nPG5KY~w{8|#N z?8)lpERSr$Ji+wlS}uVm(9eZkb{FoB^iJ+Eepi7@5bbvrP}|$|6_+7$mtrm|*)IiE z?|s&7&`-If@#1jasWvd~eFkz}GBxbCz-B}XyqfCua+qlyH|c-XCbGbbfCUL2m` zwkKb`I6T~ylYhK8ys*@yE4TFp6As2g!s}LJ0Fk2~5So-+_$Kt#-p?i<`WYAYe)vt{ z>p11z?SVii+c-6W~RkTh@YA*>FR3vTFA2A>o z>SuB>;&czBZm+6~v*|hoKh5D6De7=1q2)lo+94Yro@{8eJAR4KW(f<}#keKiADm%2 zWAIC^19fVb-y)L1mCe*{Qk*+od_4UEtzO!f{?`X=eyHc~-0uNtW8b+?9pv2}waDIZ z*7>+1Yw&tqf35D+r1jG9kXP}Uk%+(i7lP^JgyFLsr+Im8oLBW8lAvh>)jWH4y|$v&EEc6wvX|#`-Z6yMp^X@jr0@ zgg}wFFmutLU1S2e$U(>mDH9;H zVa$M#s#Q5{Q_tY=Ka@5K1PKm7W4CW-I?^Y*eOoub+B<*wayl1Kp)U3l%V|Mys(}Mo zeX0tW2JRWjFE0zvtJnoavR$;*&CrXl7Wo_~L^cVnU=YEe90)`bD+m6B>KDFD8Mn6% z`+XqG58hzF+kMZGD|5+dO<9t8GV4{K09#L@cQSwYnUduCpSnp&|C_`72~FO0LS@_` z(M`HOi{K<(XChv4IT#N5;DTZrLO9PzX1oQ6@mK*0m!v&5>*Md+m@tNsQ);WZ%ui4(w!%oY8Bj=YG%VF>C5%Q&{7|pEUYb}$ zr#R>=-cHn4nN}{Zp4GKrb*|##`5q=(4w3@Sq82$_Y$f-I#S@7+@XM*(Lw+IB9TK zUsS<;ALwKKCU1+mtgaiDFc4h4MK8i)8#8YWO-jCaMHpYei1-cS304+XA`mHeKd%+O zYUA@f9HJeYn6>tMiW3VrhiTx4kf)eV){hDBV@JuFw}z*^R+R2}um~#DTuU$k>d>}}!M36TFHD`s zAgi8V1|IfG_rdF(_(e@;lI&IC>y8wcR-pqf4<8q-RpApnQ=@FsaaB0~HH>nVAM{lu zaOJj))`Bg|Ppj&OWDT(wl7OLWNI=+X67a`wJs3V&i3Io!u2h@+{;Kd;*P67vEu7YX z@oS;=fUlM$=e{j$J{E>&8CB@=0wCrUxeX{1kj)KX=@bH%P0Etw*Y60Y&oky_hhf&6 z*EB$nIG(AU-7PKRWl%%%?{5pIzZz25Ad;Z%?yTJwl|F6A;u>Z;)2H>OEIRZ=4eODf zZTk7jAUY#C^}29+g`anZdhJK=V4Qq}M#m0;x`^#4n)Z80PXrj*y;V^A^_m%YLa%{AGFUJ?rj#F;+@Z8JIqd52^l7LU zq790=fwJwHGF91Wlu6F{CpV4la!ff*^OL@!89nwG!qBJHAqc$m31q@_DNrEFZg5ZE z0UE}Ll0xY@^ zs$5sRQ`T1vhyL?gEJJ)E@`L43VI~GM-=T4%Gj7*%eoYkneY|{0Y&-qFFw=hn9xDtX^5%`zv2ftnaFG@Nn_=kaCz_P!SMVsPvtgzLhC-c#iq_jGmS*a_s zM}qD*J1JaU_9Xs`w}Ct)DZeJD*LtzTnN4ZxEW{DP|kk(UE@ysJ60$PgsJ6 zA?C75H{ffdHNV$Wh{-3D}y)jvGT{tcI{&nGD@M~da@h!=3t_v6J z-`V;X=&V!kIG-U!FxVN4Id*4t7$H)|eCy}tl%WX_`u+U$4b6|AI$=^P8a?U+T==<3 ze*O0UPush|S5;gM-+RyO+)u*c5(p6BoNx<51QfZ8ZWKZBf|pvngMy`if`Hemi7hHB z+Eme@h!_-0s-UQ7P$LbBii(Pgiau0h#gc&wXai znzh!fSu>+rclv?hutdt$G421q@ds~G<1aGZ|Eo@Q$?Q~99n>IlMSPCwdx%B;B(low#yhlE}UJ<{y`+ z?!5{uK39-T1UT3}(! z=^{e1Cjy=<6ai3$=OII|KDUyt9eWxCu?AVjkpYj)MA`qg1dw%uG|;k9K`6`lU?Ucb znHqMApfHKBSU946%`Q7M-_C}?mzchDRF~FvDaUdbtsI7mqD}Bk-<^K9Y+v*RzPV+N zN>&yO^=xr)#(4l;014QeNh2u!t>TVm3L`*v60r%NT_PYui5^v?9F9;D3H8cfDPK1r zVhKFRaf6MS8_f+riiUid-j}Mw{0?T~rD{xOgT2fKBk)Uut%8G<)Q*5D>Xh2qtlvZx zh;VEd_ZjLZ61rC#u^YE>D4`s|v_7W>m?v@22ZRN?{I_ZV2pD*n@g|Tt?=sc1l7WD( z*){DRTNISc`gU2mwo81F&nnjE!aV1RE7Txzt6Vc=1x)DnXa#%)iR3OR3KIHcpG#sW zc8DKAVY|2##y#F9>vA=uJt_bug-8qZZ$2A%;#OyuX)_La!CZW~%BhT`1=}|F;3y>0 z`rLz~5Jl^w7ZCVVpXddwKh-B%i9lfcge%MPkT=*kd_fvnTMK89VuW9aOB=UD#a1?2 z=-3_-ESd-KyVmEttsHK$wI& z{h|fd5$r6iAy8D>_WeWdpJyKtnIV^nDevnssfiGl!XKXdU>B}q87j3nBBWb}rT)q* zxk&$(vWWSc;2je)j=@gR$B`o@JN|OOPIvyV>zetrx4j#U5gEH3HO*69!MORmd z4lZ-i;$0ayv^U4IZKK4>T*xEWRR5T!E>%5&_xDT>|CWlupU^TIytUR_W7hrN}|}*61cV2t51Y zfw6@~L-6ARQ|ZPzJupymk%kJ8h7gn?PiSix_(*355%k;?k?BoRBp}*l$qnv2kvw_+ z%DxPUm}H}EHxNYUNqc01(Q?5`JCn0GcUwzj6)yxJ8KFOhaLZ;hy_fEZgv4Q4!6o!* zGCwnB1%+hH3J~7T3Q~fa8M-Vxza_0SKvIb-@wlNG_zQKqJo#xuL2E~vYJ@hVHIx>A z(;78UV?hNwJ<8+EOTd<+fkTGdn+ zt*aFu(v~fqT6o~PsTNi$wiZgiT20K(2UU>1a;{Ow?B9h=O~q14^DbA2;*=`VSOd890US&Tt25G?qU$NI@u`eDhe}9IX2^wv44#(}(yRtKV7yNa z+t*~2w^qvAm>Ke!gsjR8X?ue@M9p1ej=O=!+#2dKa{ulI>aNKMX;wFTI{VhW_sNXh z-;q1EB{Srp8>Q~dkkbiKbDuO<+^EK@El-%YZd50BW?-4AXlNZ)@Bor*zSvO(Wz2|q zpgSqni&P^?H`v`9&@nGWB@r*P2qf_iS8cS#-c1(2$+6jq^?Xer&0vemc*fxwl=2)Y zSGV-R7ZoQdNi)gDOZ}1>=d_Frp3NW)*@Ye`4`b;#vw*E)_O3-(vpw4jMhMEvJlUPX z*ajN@oM?hcosg80_mZe;L`F{{+PJF1RXl>!%>%pa6qu))rbZRtsj0Cini?`QcG-GT zmWz2I)rFhqHhAIs>mxhLls#Bp*ob9#b3?!TUQ?=TW$v}?V>PmmmDSBNf4@l`QiKEu4Bo`(-~iR0MW z)*`{$p0_~F)3>ND7Zis6w4&Uc?5sd5-*cChAk*c#J*I46PPGfOWZQ*fkVV)};Qwuw z-DiT#OUr4G&A~>KG*OHLu;(F2T9;K8GsEYr!~JYi^nyAw7IQhOh>n@Y`RXV?X1d*~ zdZi=S?1`B(ZdGUbBh3@Hs(v_szEutNdz<8Ks$U-a&Wm#r7&MzRZ&O|Jki}Mrx4^V~ zNgZWgxQ+8{xv~q9g3MoUQ{DVArs&t|7w8Rc{*s#py^OnT|9?)#})`6{H2s)j94q5T4j#kKOX3$?oExm&uAR;?+v?$xmzFo~?WG1|-+WRB#x>xz45${L!E} zg3E#}*Yv$pm4L2e?o>y2vYp~JHQ5#|8L5)paa;4_Cb6VvK3<~Q=N7sh(n9mfovM2Z zzgY=0+TYx%j-aGrcd1_OQZvHh4Lg>CxmZP+#1krE?!HU4iXSJG&fdYyx~VXJoO$*x zHHrzLjT#&m3=!$b>gv$%nXyLouf+ayq09?*^tOu2wAmsqP~9wU4_-|am-(|nT-34a z#Raz-#6=BQBQ6+o2Sa*4-Q|ELIw`gyJ4REmHGaB0x}#iYAlArZ>XeS=8>3qL z#iqqVb!zkZc=1Bjt*sqUO#E_XcR5_aE9zkZJDN=JQuEPL)xmtYQ2lJ2&>(3*wHq9$ zN`?2EK(_zraufPAO=4cgOk+t3eK4ULBbq6m`8B)hRHqJrGMy-eN;XlE`RG11$Sf)d z@=f6))pjg>wPaV?$&?-|BU6=Y>9>3`jf=%)8W%ZBOpsjq^Re_NH{L`S3JOf$=T%2@ z(;^i!-#@3?C7a^p5wEC%<5LwJQ<&v4Y+t0h`V}U2x9VBclm#62Oju0|D;z#YB*$gv zsI!teO3N1KVv2^lFd{e^gs6vv(*sMOgb(L-qxj_^r&M;mVgkQKLLJ>Q#RR7k+s|j(YCU$(XrpiR#fR#yron^RmMGSi|yS=HVr(U1_14sA4sdA`!(AX^7Q9 z76LzhLgsXC6+c0yb5~PRqhctg?2~=1h1$AJe>Y1u2ukHKMUJjf{mSJSyqofDQ;T(> zTdbug)PkEyo#iG}XN3)2eU0khpRH*-#UTZDiZkWwWzu3_y;@vI!76c?IhLiyOy{NQ zy#IdoUI~Ri<33f|=Ri}GnV6n^nN!p(y-)T00WSN;`_#!8{Pew_cjSO+6Dpo_zba>X z+;G1-F}t;}pO~E%1bunGYI7vlc*=z3S+y7AiGzOPK;T=uD!XJXYDe0zeO&Yh#vU+-ZIq(>K8nqrnj?0>Lr?u**;iX`D0jX zAG25!VXd1zYb?g1lZ2N?DEO2`T7r_$Af3=p`9@Y{BTvll@19H>RjQO34 z^-dM_dvKi1T!e${P$yf$+j;`tyz#jAj?s|6OAA4Ucr=sE)5}$9Uhk-a0`t*w)sFk)WpBqV zvkJoaWvImq!raRc2eEN1;CVnMbb;*V(D9^a#kxcnbjdP3xx8V!xNJRb6SpTX2aC(b zh2`)}bIakG=9?tEJ1PKilhJg?=C_BP?}=!h!vt$30gHoW;&LOmW&OqG?poD1s|{Sf zjd`P19hxQuHCafCMZc$Ca?hy~?GF9D8r+?%tAL$I4=cn)E~v!~gP~rHxLC>|MpcuO zcgtBhSRuSg{5ueFu;QP6BpUiOpZcWGdE#PYGgn+zI#sz&_!NOYL!fX#hap-B!;=NV zj1eP$fn|IQJ&!fIAs5^hOUyKRN(l$H1s0Estyv!Fk>qWD7%2Js`ZweWWCp~~#mLXdJe)yp3Tv;K8gl$~|+ol^xyKMQOWUEA5%A$HE zpk?cJ)v{IiK~}uN$sKeXthV9gzqgeq@EWJS&XPndML!2u@-> zYN?kMkZF8DWPd14hJZoJtvO?0pRt!_Q#dcTVJl{r>NYPOveTfL3{Bm!ln-r4qR@Q4 zQYC+i^@6=_!&0VVxzkRRyRuDhT?n_OvyPU9mT(m$p}xg#K#N%i`^g}|iE6><6vhlR zlOI;)BP2kKW>Q3bgUT2|8KnyEW#ty282Z_P%c#w~`mmrvqWf|xp$DtvrqOg22S?+Y zw}1CAlT25+Nvgz%Rrsnj@CP8?1n@k7Eyz&ey(vwLF*R)42rD!*A7Nr;n_C`X>(5R` zu>bOeCQYr7WZ>!Of2!x*e1FN-Tj@M`vLe5WHB40T9LvIl&mr%9qE}Q zUS^GM(X&5p%!`B+K?tBp-4)(ED{G)z<@+Wb_3uZzr$&0o9+lBTEko$3ipmKKZQ1r7 zFlhmcBaO&f!JN(ZJZKG6vHvtdmN4&FfF%MRz>*qA`pF)Y-dzf}E#Q8rX)8y5@Bb>< zT~k!J#}3gJHPUo>OqCQ7eJrI789NKUP-sRyrpEaB=C;RFC!7_JVFoA4eTylAW|lCN zHX+S^e6;y+$XL-fhyX{~BHRFI@cjz!DY5s#U87`zd18aZP!*3te?Z4(iHIfdV_><{ ziltOOD9OQqNQiQVFiu#KvuRd5g5pH;M*x${^)W&dX9=-srV_&kAJ5Qzvb~+_6m_d; zV{iuMqFRfaAS~|*HyovTyRIun*61~$YQu32kuCx-Ys9|{|(Ah2W z1a|Uf*cx>~r7VyvqoIe~AD6Levl56^W3(1*%viNSf9$HQr-Wy!G1vkOgdbV4G(#3_ z(w+#R5eu)BqE(cxEb12I*g@*`lq&65(WJ?3*<2w~U?x1Jy5w5wTFCg{_!Nr%0`u5Y zYCX>7PpdOh>%APh+g^%`I{!BLg+xw`Wo)Ia=vqql##K z-7{z?v(2JsRG%zSJG7CNT3a)2hM!}$Kcf)uoW8{x;+kYVTF7iOv0hZqO^TRob634O zyxlmr(vd+EeanDAYRVu2o&Y#%tI9fGUBb7inC}#76VtTGg{%I;t5O zvi`bHRha&NP-R%-_)d})yvt6rI1~m&qT>@5S1b0)ZYKo2L4{ZA2EnRDfA2@MKNE6s z$J$b5z<%Mnj;zy5>?iZ^wi9a9o|d#WCpWP2YA+;+gkG|}2npPzAZ#<= zjsHB_AjL|N^HN0yE(t+5QDnprL}@5(tg>q%tHR8VzhybQ>;5rv4#}>S?Cg~uh(7N) zA@EENdj}vz&_#hzp0q0bqbLPgQauGx_&v8EP zKFT*&t%Ii(qQ>lZY@zDFhM*7glP}v_S7rWm;&U&fu|D@2&YwMM$c^`y%+N2zJu5wid`D zaf&=G48yMU;ShG#!ZGZGMTM{+N0uw`Q5XQD;Fu*GT1rQqi~U6o%sijfGNdZ{v^7&r z@mFoINrrYysaB;DqjjUlsa*D8JthwZQ)gfQFN4OnGWScvmSN($uIw$(? zQuF8MRiBnLB1Suyt+p8``7#u~KdL&jqDUhJHz%bNnp4t=&3Qa;Uy|Ox4C9x2ScaZ-dp*lb&`dO<@o_ck|>K4#{;ppHaO{=^HaXKL?92+e=CxB1r>_ubn( zYkz9#wl&W;-@l|vMyEDQ7$(?+T?&SxkE4+XV&!O=(iE~8@jQ3K4-(IO6R%dIGjyIJ zB-)`&?RS=2={#eb;XwPHoM4QZ!(Rq08C>{>wmZB37^?)MbXYEYuk00dP;tZw6HVjS zEeUsfMRjYv?_OtnWS@nvs6kFTmBVE}xYN1o6*c34JDu#J|L9KVMdsHVRhx;afnom3 z2{wqRoNsJCE=?*hDB*tNVii!pIvtT>X*NQe>IJCYP^JC*Jy0|8HT5ilN|)Ew1z^vw zURN8iPdNS!wU@wSH?hl}ZRT!LV+q`}NuAR_fJq1s<7iK^U?eLIe&u|(Pd>ocpFav@)m=YKK{Y=NIH>=j7%=D6reGmqaYYq=mh!sbx75z*P}gxXD)x z>p`OAQ*1=BhhXDeCov#pJ5=<;DqP6L@XDI|6Sh#7StGf{8nq1et)H~{QoJH@bQzr?(6r}_p!g3GUz6n}2 z7Fvt7yDDO#=Kg61HB5Rf6!U;ZjpD9b$s6a$1x}k}A%{`yst_-kb zS;ZxZT&4|Ok4Y-aH>|kC{kgf(Ez3!+f&vPJ1VY?%<*IcwK|wCv^<-6Ga`%-> z=$G{vsZTDfB%C`A(M-~S>2wfh3SGuI8|xdK@eF#8k?Q0^inm9`dWl=(Cl8X?xo~cA zs}Q}!y*k-JoGX$Y6L~bxT~I;50%R$PsP% zEj`D43H# zMh9MOB4F&EBj_0Ho3KB4uVn`>K9Ru9s@j-*eB)W_e5F7}I7BXoV zT6j!xpgi04dOQ%dFqHQibgiFE!;sfqEf6iJ4*1wTgI6+Q%*`J{t4`#w zXjPeL$6pd!$fH+JJl#hg0QB@9|34hvU$1xu4ul zTRV(>@B#30c^<({aTz)}&2Fc@*D~-drKL-qX!RAiPt;XX*kL zw*G7c7qGDFXFs`sg^fQ?8f~WpM{mr9AFCmig?Oix71|>>7%^b^D(p?>bP7;FNB~76 z1yD_m2ipWzV7`zzVDX=V285&WB5vgN>aYLf*2}D9$z#6wBOA9gs)ogKi%|(Z$`Xzi zmJqc?k?H*IW>Fv|Top$ix}a0=Qzjp4R^*r_K?M$uHlA~17B)PKfC@Dm@d9^ocCs}T z!`@1mTjnJTZ8IvvHQ7^PCn~JP<*nMJr@57wgkB<-164d|S|;{a)Ln>hl`I#h6w->* z4ZP$yR3Aht(hW}bgdH9^t*j%bkKkKM;^OkA17~;e$a(DE_NI_q5pnyJX8xXA{cJ#WIL_-R<#5q#C$xc3JtzL>{Lha}wQ`Cu8R0kJxVU*#(Vb zp4FE9)0Q-+e7Q^v%Db8foGXaKnUidVc|-SvTpZ<2K@?F;K9p9DePRN_vfl#M5(GPb zKUz+l+%3s%u59G2V%|#5Tqb+1OytPNTt*``HrU)ic00=S7r6w|Fc`0-O(Wpqq;xD0^;dz#y}(TRSPiQT@bIFoot4l@msGTQOq_g5D0B)lVQ#<$(v~cQ zs1?E(n1+IMFblpc&KIU{$^x@eLDH5&+q~3WS*^+DUK%T3o&#<&hDE0PpJ3n*nuq_S z&PDt!{6q~rH6~NN4Y0+DGdMT_$z}daXAp_O6k+$Q5kNXf1I~1^w1mMBT_xHbi+R>N zAFsLV6V>%FQIiX`VrnlF7Z5|TlmLFQxR}|l78kjv9#@oc3$yQNe6!emvrV-!#oH)4 zX71Xiy7Zz5%pPnOTNGKOD3P3?lwtwo&hF%!54WlF442@nVyENu%na2hlo|D!CaWN{QXb)TxELG*m{-KT2g@O)7& z()ZjlRvQ4yi9T&mQ%PVFy$hxtBGLc@crXx6w3!N zM`c!ILxl*h%}!dvxGWpQ#Se`yN_dVXz*U%1*>2WnCn_xw$aW;b-d-Ze0cy(Cq+p=n z2qb~C@>YTpU7dXjy1O{J$sQmf3u(A-HPl>e@ex4?fb0nW<0}!-c0RehYfq41%`RK> zZoQaq?;vE^%`A$R-FvX*bj$9Tx#zE_RV{>803o?M)%IElmW3J=)y6I(RvOF{l9tkL z&$2NK3d`mI1r4%n&w>V!z%*=?=o*p7(kpv8<_f&iwVFYnsklrbfw!&M)m_F!s>*hi zO5GSbOWg(LlF!uPvJ9UN0BFe+9OmUDoVZ#488;FJ=CjY#xn(cV5TTXCN^)d1x973QHGY9RLIbVLRnQXr!ZVUWT=0WzBJc3@g?g=zn}I;4^z z9Ai&!t8feSfPof)j6JQ?SR;>dJUoRF8?@j^Ib(#ANcIpFfT-|g%>$lVJU}Tz^AQmU zlywJA47tQ1M@l6V;YKCxh7Lu@Vef}TC(|-h<04v_%(44lVoae7_I?;+4t`7Jw?mF) z?{GhCF?~N%#T6WgJusp)N(9)sFQT+JFMOeTv}btciZleeREZ0|a5sTM^a*yVGL-vp zjiKJ2EaWg>Rfe!cGi0YaqazKK>*N?u_C7xpe4+%)5em%8ovMG4#W99*nUt1ecI@O_ z2P=9u*nH@g>g7S9AZqrrhRRyHGF;5XBT~nU^W8Tn%ufryr|@NM#~?o5j9hA|ipH7x$6NgGNe}Ci(D^t-I7o zJuLG{DXGAhMTmb*Tvv%ff=XfeZxD zb+^#W`n#%t4%Pf!m9}-Ff-AzdygH`<@%F91tMXzy7*m8gi-eq)h)H)%i`}YIvCpSF zqsq=rE4L^qFyU_1Zj#VWy(!C)6+XA{DKrIQCldg`2%%wNl_J(s{3az(NFNG}Xp{&R zKU|Zn;8C4ap~3P|Ik5dEH%1|*AXCRb!R9#iKY{Ahf*DtCyYwqO*|(BA#@WdN*6y}( zd&9U0A>0#(5MrZIVc-)iK~T$;Fa|mjUMhWBCuJj$9WP&&;2tf!hjd<|z?D^yDhol& z<}Zi@nta)9YATovkw7C6BGA|tH01&}t8YyUG&y1A0(=!tqOn=_Da#5p5+MT3K4ql@ z8XIi|8n#R0e*$PtzyeB1!E)cOp#*kN{Xz^83uQ9oaNKxQB$(SR2QJ3QmCH8hx^!G@ zh3st^95TX;#^RoUka(7c4uk?fx*Z>GD`k1krR}UDLYcfwCv;-6Md@WF^7uPC{ysy? z+KzO-C=DywFNl9&!;Zv8&<@35K{6Q%%|mK|#UDFDa(3tnsZBVu8>%vR-AukA%#&oy zY<`9^6;E&!I9+m&Cjm}0(38krsWDcN>_NPP z(r9F(33TLx+Z(*Fn*(xun4}gyW-sQ9Fsg}#+pf?N960EI+i0AG<9z?TyPq>5aa zI&G8X3KPUlUw@D4WTgPODN;bsJ?h-UL<7m=1|Es9zM2}|31WqXqB*y;8lheG8!Q!W z14)Dmmmumqy+?KAp5=Gd()OiAG2v;^GEE%=TH$s1*X6&_NHC`&sLH=7m=MeM&cNf3o*vDTg2LJdV= zghYzK2#K78lWSso**X&5L7^Mjff9F*~5a(muTvO(o4fQ+xOeO z+)a>gUf!#^x7;ta`F^iD2(C%u0~Uh5490A(`%ZPvw&r?)x#(MUTqhO;GijZjDNH`s zWM+mmxzKF>R&`~?$G$??IsTaMptLxkEwhMGgM=H2d9?@?^4tQ9@3NsDcfTisVNedcfDr+g z_6qxof;EgS1q7TH7RcHXt{=uA#kLgTY(fb+m+gh0-qYaq z@;nj7yzp~8cF{`Nl7j?MJ;Y86EmikPCsf@Qh1SIucXzdu`|b}N)E(RR`5#V$}- z*%rCY&vHR?%SHpUQt6XhprwrHjM#^^Glg1r(`Z;tKdpO*%}9!!*33{#1(@b`x--q2 zNL7OrfxT*aT@q0ha9yYhXev~N2_RFnz--cbNO2J>8Rpc29F#V=3v`K>rFjW?qypK)XgP)-HY{&&DgvPc=xnGTdS#Px+(2=8;+I*Pi?jqX z-OA*;71yRwg`F~rgi16OSxu1zVsORvZWJKbyD|4V+Cvdp*i?kX4Cu&%4E(Do$fa{?HdykP7EHa|b;&)bSU9r<~!Q3vq>} zRp;nKDpO%wB@WuZ1y@WEA_U@II!n{Jc~1l{_SVyjz&298%r2HlZr|L>@`8`hq6EDH zrDYMgC!j&n7DmHRby$t18TwM96Fup)tY2l7*ecN~wYA=Y{k|HBSE0#<#;h^pa`hp8 zH}k7p-Te?t4_Q~SbVP83oXdx$bj){TF~aOslC*9OliK4YlGk$ez&yYR_hI>9VtKls zIj>k(m~--UZtrfwAU@9y&%DCjOpy#p@EO6&1>~fjp@^*DWxKXlcQGBPKg~(8L zyD|e3M>=(Y$wr%SZ>v6Zt>db*h^oll=Q&B>&)UZaE6rK?x>Fx3xXh)!(iOoGaqZgx zRu0H)PmZ}aU-!$lHRPCg@^zPv?!6#cR5>L(mUJ!MqA+G!6zBoxTBZ%6NU62RH(Vpc ze+0K2xhsV`pu$Fly@B0~?8b0`)ADU+n|#>m*(?MYilEjm!7jIXqCgL{bb`U72B;%a zDgsb$O!vx@StXOQjTwr!r$t=Rf$=J4>qL)4cXl$^zKK6-vpRu}VK%Q9=`OZREHzSP z;% z67IC_UZP}WqKn(uN%V~WUhtjyIbXWd)UBTu!Xf{oZm~>i)>@Ht5Hz`$UC0h*F%B_5 zvK@>$Bw*@*LdX-MT(dO4S@p02*~3YBnA?H6dkj#Q+J$8K(1cB=FGSVj&sFMa@`JdV)a^MrgK zPcw1G;7AJxa{sn%B!AhTi}wHkJTzJ1y~fEb!3ZiJQx+5F_%eC0Inw{J^q@(iI3bN_ zF*?{ye4*=>hLip@63dzy{TMew3E|7JhQ3mwEe|Ht(u0UJ3XOrtTdOST21A67ZHv81 z6J&r1q3c16kryzLm(Iq^SdW)3*cV`Dx-Ue^DONAET{@ELM8q5NA=K17KE)dNo|D*aVO06HZT3vN(?D9$z^X}5C`P=)vM*_m zwQ{hLIm|+I8ElVdFpMr441>uYk??(KotybxW$MdG04t3(BQQu~)+(Wmd8oDSJSI(H zevD5zR?tMr3S2Fgz6g!?V_8k~rL`ri5$zF!)>vljN$FS-hy@#4NjH5)MgFk27s*O8 zAE3ibTEAl%yp%^d7#73?PXgisZf4smQ`<&&8z;x?oty;6Lr&s5T6om33zQHl#54qI zwMaLP)_Hbng^jw|WU<-G3e2El{S%Rltpt+@DKE-f=yhN|r9>lSR}qbzXvOHfy$IwK5hDmIt#KR1agr#l zbDxWNy^qelC1R@8x!)S8-0iY2Qn|yqM8cM5M&@eage>=RsW`22x0(nvmiwvP?P_JN zcC(WQ1XHl7X{(C|6$({B%1gs21tAEO<#$z@)5=U+87^n8BEp&Zq^<7O_lLzWH2h>WNm2Da0Ov}u7lLMU00)PMQIFIZ7`sWSOg;EQv1`{Z9w;&l26PqujU$t(OUb2sH|W7B(ELSyYMX)m|Uc1L0C^ zee!{-Y%a5|5!kbhTIM%dE%@fT_PX^^h-I9N%BI1>OE~-@b__D^Z0^t!VMLHpbUikj zopyYJv#{<+{U0;$wb$M4#x$a9TK}|#n=wTH#0pu|O(0i>x(VhfyB%;%k#B5Gi6U2* z=#^YkY&e6O-X5?@rc}jj?o11_MA3p_ahP3+re&XO95A{82_!Oj@WY$&^!2b?fP+Yu zrcT(hqU`~$pW45ulp;y;W!gJLTLesb^REs%X1?yJ;~4UKYzlaG!m`R5#c;FM1VLF+ z%UJ{nICngWW5J8?LYE~lDME>)f-vPuNRZ&#Lop1I@e}DDo))jMba7#}Snt`XC8Uux z8sF2(3Rt?$E7x7na@LmX{&_HOy9w=^ogMX1@!D0SZ@Tx=6VF+GM}rr3r|d@f_J5&= zrk(#@4{dgp>SC-aJ+YQcASr=<@Lm9bGu?78KH50HT?I(Q0OxntUy8sf6PY_`>_nEA z=!89@;d0isR-uKae++%#ybwt1j4Tjx<*R&9S3TZ+vIzhcU<&{>mfKLb=NM2)<(uW5 z^oiYageg|1f+odG`_B64JXp*|ODD~Ro%INI1uYd@mQ!X$>st0q z(HuS1S)W@VcT3VHG4nN@y6Dp?ZOM|sW1zdopI|5v_~c>$E})Xd3`|JW+LDg#t#|A# zi3t?HuV;xW-R}73r-KuD!~7+}%y`J)9tc|0<_|1_&|Wd#Kz}3)OICdB&?MxNYB7{z z6Ve597dO7fD0GwAl+YbHl(HkC+Yg*4S`V_iG?r_Uwn`KdZc9My4*(MWP26q)h2dmO z(hN)L_HEI2VP2BMU^*K#g=oa?%G9L(X$u5N5s>6TYow!`x=l%)?{nS1*;=N{vu#@n zOnz6rs2HM=kiqi!#?O{|8lUJ@|l}xTtY_cG3F_oajLd(Pe0fiTirDYIOdMAO={(D5r z2(g4bla|r(__8KiCPt7`QH{wlj+ixwl$Qczvy)S&a!A!IDA%n~nnTzA_TOxp{YS*g z>Rx2i%<*Jr8jNMh4;WhxNSjt3KmjHF*vHyZWQ<&M%t88ci(8qdhtAN*ynT@Vqrc2N z+FPIdU+)Nv=%Wva%ypOd(Vr=Qh&jKnp5hOyd$q5Q`+h&u@(#as-OhgcYM&E!(+|-l z{>ZxP57C{qKiDigOb_!fscSq;pWwIHw04~xSx8e5Zd_}4FX_=Cn+Xi%kDw@Ae452G z)BzGMe+cJkEl5W~kCkHx;TDP6vext%tjDMgYt78Tx^Mdp1j_AMUMRI#C8&t^>(`n! zgZ0GNMhTQ>&?Th%5M7Z|FQwG4tvh3gF7?@Gy=$LG+ZC# zH<+Ic*B2AIez-nQLc1T1=KXo|O)fAy$_mkAt;e5I6GZ%wkuekVw8gCh5Aqm zt`Bv8O8g?!*W;Tr()xZkQs0R0;G?W>!clrYzR!=+qj3&9S`Q|}#YgLFCGcqdOPrab zY~b%l>7Tbp1;C~ENPZk0hw1T-h6>w=irVx(MjxXZA2PF#(dUrq&13YzYWKs&AFXG$ zp8p7N8AP`SvxyGzG*x%6XXRxIE>=*5a;}`lpvcuDIS*8Y zz+o?G=F)AOWs_>k!fG|jlAy*_lql#Z5;Fvm)rftgLf(t#rFbN2xs^c5<~ay1M_?1* zit+Zjdm4NHV?Z-EzKx2-Ziu;ROWRCo8|-(5w?z&0cz@63u-83>w@WTAOMy#MDjHiq zZtn}A0PPm3S%9dBm<>vlb8>=qqxt0t`Z%?bw-GwBvHQgdyu!G2rRjO1o>3S7@*p}H=srdhphOG;QCfZ&5fWA>k_|$l<^cp=b*AvUtXF3tDVR_F-E_(tvapXNM zI+R3Kd?9tpc(9{m$6!+g_C2_%A>AcmDJ)=U(^wD<@^7T?}Vfdyvc~r|z`d0bgA%6rxs^8~h_vdEqnfj&p-X-h2 z_!8McgUyHY?=^|Dbtm)KS^C@qMjfNJykVxCt$!J-2UNm$ym0k8bHoIVX~H?Wb*y&3 z;G-mX6`*UL`br62uwU>a5?sAsA&ChD@BXxTAqT0|pPGK>>Zz*X3$yrKeWx(kL(kI- z4p5yfcmL{swA@i<)dW3REna8ZP1Hv#1hsP}>Qc4!9`lQd`gpbD9uuCg%go;<>eC|6 z#cKUM=F#)@F>v%f=j#!zcdZ51g8&kuK;(v)YRwyUhh3;Y_s8%4M0(W^ z+*!R&z6Lv1UXD}F&~1a6E|&38vf(hmMGnWAZ=vx+`4)UychwXf52V%er|W*=uAQ!r zZ49ZDMUF$Kd_uToN1N%`RwdP9d* zFXBzQBuhyWRtW%0CBETGeX{hT^;NDnqps4&r+Sku8yczJme1y}s7mA(EmjMD{84m}VH#SQ8WZk27spL0i-S4!G!mxL|s)8O`L6hBAkYFg-@mw3C-@DN7TW`mkrRDy zG?-8`1Q|IcJ>&oxa_xhuw zIyXxKTPJFuaKXOG=cb}lNV+T)fW|2Z*S~Cv7V4dYQXP0DRhS$rRILADNU5CLQjuYe z;CnLM_KF$sTU}leO^*f4l2l8wuq!VULpl_QEn=_HwTRLeF2I20Eq^JyS(!x~Ixv9oLqq4#U z;)_b(fbd!kln(DrqWu_sizkd-X%7L|t3Z zYyh?i?pQv|bs6DYiL=we3*i~5zQCdVa6#G^6}0PFb4-mME41;B8d=!=(tKK@PlzpG z#>vR};oNV{h^2az+H{?{eyP48MK$e)aEi#7F88sf+IGA7@;-f1>t)}k8qn}92w55~ z_`dGc`#Jut=6-J$E!U^C-nMUw&EK2vm+N`$*Y6v#S^}6xa{qL(d7@TN)N`xWnfAZe z_e_jR-&TbbVrLp^({Q(BaO?u#0*Mn}7$shoPRyz#T%C#rkC}97AM5pSGu5tR>=3l*Ucpl>BE72-KvN5qkcs+CYHxVbm};m;F(IE8uhUk08b%YXsjL89zT=IqFCn%8EDh~8rJx2SW<9KXr=$)! z^_3ppqaM}|sJ(BS&mKnc1A!j|C))0Ow>8)Ae!oSF3Hz0Q2j|t!n1^$931z)o(U9 zJ*?p53Ov2Ulo_3Fxa>`%R_p7#r&ut{pZMLfYE3cVW?fma8W#QG7O9d!8hIm1g@#>q z->yb|lUTM{g2%9GcGA>1z6dxH=g4W{ip}QUC-trIZR-Jf?*wYIxA@roZr$)YJwhe- z4F+je2;w!qN2>PW_ATb_r*xxQ{GPeyX?-r7Z|l>#R+L0pvZ&>Ky|<%2W#PH|ZFA2v zIxIB=mV2Bn=Dn-1I+a3tJ@0MP|5=?h9qV3+CAzcFJd9j+3s%y z2Cknqj`>tQXTJG^o?0Ru3j}ie_UoDkt1#Q!pu6_kP3{UgTf~|@+=0gqsx*09$|FtQ z`j#29P9KxEOh8#0HoRxdI(>YGcgH$?Nn$lwL~oTEy>h+?D8XSFv+7-Q&2xH0V(Ys< zmTKF(b^r4m0-(^th3oa?)NHUtL^8OEzo0M8s{&WTWVq=)bK490IfnD(7xj5aCXc?T zhvR(lq8{BlMXWynMlzpM#C$bibpa93=6+x%yrdVUxZ|=9438oAr_jST=*fvqX`J10M4)qDcSOYd3pnrX4xY1)doF4)h{yTFY6&i3;>fr6@)RXbDZ(Au1dJ&g22}y zxPaZ3gq?>h7tD3<=#r6caqvT_7O-@%i;5R~UspPA<~ zpZ;B8rs!2&Mo;^{3WC*}Q(o1h61zTh9hT*xe3c}iagMf1hd(lPF7dGUbx-sCt8C2H zn=Tu5cbrihxk|gcW>0=@$TMe2}${Dy6e!zjS-rah+LNPhz@jq$|Jj# zb5kCB4GmKSz3ug{aj#o_qiONFzA5ThzcefdJ6M+El(Rhl)U0`3i-7yr*LAXx!4RwS zXe`>jp(hh_%^SMwz-`oHXE+MiaBHekY1`(MN7}YA<&n0nZ#3_|q0dosKbEGP#5H&N z=zrgy?VI$$*)${?pSG<&U}sDW)x_2`YT40ma&}>vE%ron^jmuPzc04tE!~06JoA=L z;{5R~JB;7GrEgAb``8WRfw67J$ENmeJvB)KWJeRlp^UI~WY-l^1h=%Pd+27p^4}D; z_v5?X(G&kO{2uo2P%^^ry~?T36ZYh1)Rz6aIlzp4SD)Tx*(X2N*A=Gf9o;^$;ZHx7 zXw#p}*YE1t0P&*tSYXzh-@m7a;%s{lSi16M!RDsK7>8|wbnxkopP1XWXf``+1wWLk z{u8O7oB3=DdnomW*~5-X-E;55+i|CC)x&hBMp^!4vF76_BsNn?c@P1~S}Y+x-)R~? z(mlHEp9HfRNs>zva_5lS^!p0l)VmRu!rJiSMr47{ch>#BQJ?82qC~fF5{)~A(4^TT zgYuq=+;QEwv##r(bc0{$MyWK^Int>quK>a8-LL~venj|E&!K+in_9$da;t0Hhif^i<|eAzA4@EPj}Y6@f9ss zpYAlhcca9ijmPfRowH)#t~=)*E@&{bzh>nN-?;BDmp@0c(k%k23V^s4?d)A<{Hf~xtDXiep|l>9aHS)z^S%w zyR%Jj?Gf8|fHN(^cR#}s0|u0!dkBKbx*H70; zL_dmp!YNW+8Diz0Xewislu>xVl$cCOO4;&~+P;v&T7=(nPsM!YX63l5mlo-%7{?rXO8@Evhv;cqp>u4C4^w5hpISGuHW z9We_6tE}A*C@)&|b!JyXy+>w5c~~az1gLOnq3y2Kr0 zyymFFS0is1dBahK4@BM`@_IBcuKdF#E{}2cQ-v=^-a+zoxeD)f-pY^*n8p8>|Iei! zY;RTyA4z-dsgCO+@4;dRsKUSQc8PgTR=giX-WKxKh6-PgyfNAHP~qc|x1+UR4i~1q zP-;~czWjGr)*DtXQsJ$Uf4Cj)aPwE@!K`4o+W3{(o)ui0*Z4I9%HZ+rBaH zwF-u0G@*U#;9Wm9mm{hoj;pZ#EAvg8pucqX;NsvUh~u@z!QOUJjk`(b^b(qV|J{4* ze$zI1CRf$oVNUE4h;T2ePhES*I^zdHiFvF`a7gPN3)V&5!6H+-^Wg&X&n_qt7TjUl ziNRyT9g;vOGUXhd2u8Nqm<}OAw)myQ{@){iKavOv)#f|QS`G(`+dCPIRXeQP(cF;? zMyu^>>$WC?)xO%hpzijrL77qwYt570gX7e)XN>9*j5tc{L&Wq%R4e+jGVii76>583 zIQkM7cY+`&5@bV^ls6&@5?=g!HAUto1^uc=aB+_)aT&4FuPZJISJ$t@rU!#ymyefY z>erffJ%dBq)Ju%iicMi|nNWt+_2!(OK?g;OD|-e=CktP z!S)dxE|IS4W`J1Pivd6#^sipQPhzh7$~EAugMv=dxvLHePHT~F^;q<@SoZw!pkQ|Y z6qKNUY*2m+P#D!RUy?|aa9^?Nv7?o=_C442#<)^KWUJQ`OQT162R)AwBO`l1Mp=H! z;0W6XNfB8TxdFHIHQ`%ZBXy+wwG{R%0{f0`UHK{rnP2jpP*AtqqKy; z+xjq8Bzmw<|%!=|x!2{8t=kH&t{H|X0!JV1f-GD#nhm_Gf2&c)X53N+-& zbZLM!zS3sqVZn$VnVl1@y-n%{FO^h^`Sh@$SFyxfw3B^!JDOss<@XP|2&l{Y2ZuE8 z(}e!6PnG?Hfql10E5DaZZT}5;8UR$d+Uz(qcyHKV`QFVR$u{pZ;WO($y<&*msb+;p<-;@KL-h)c3cSL-hE zZHuDBO{d!j@YxTHNy^b zQ~j*Nf>Q@&&IYk!XDgXZLhG3lCvX|{4^NLvI>^)@sl9R)%Mch*sCk1!=E%%}!71tK z`|`k`yfQV#-HB2=tz2F=!z={XOM~T)OlUDF1FR5MKR!=mYGP-xO(%^?H8e9_=+AO& zx~XMu)_Mc87$C^XrGtXw`qfKz`9n3~wdhy%WtU$tT@{7XA*}4K0J8Cj0_NyQiw6kM;lDX*cpwtXJJm@QI z=x2v>91F_+o5O>WG^1imq24@kcyNp*>oR6h_7jkDwyrht5kdQ$ZBjV!>o+3k5)GBy zn+Fd+YedA$M+AdQ=P@d*Qs6QiK$T4#wpzsY5)JpA=JgT505$(E6C4qgRo0MF>iV94 zIaqd)JoS6i5+ey)m?3AdB40{I^O+3+M=F968F8 z>fF&;zO(F6)Z7$MoeDkzyj;-hx#Ac|cE&kPHf>;C=~2OQUp?`-dEw}wPnWfi(==IU z)$kF2P=_x|Kps6|)xG&~6XzIg=}Sup!}yW8$YJT0S_aG-P=qfoG3Sp8&QyDiSvx9d zujW5v-X9h8OKe`40{Alf)g0hk7n)wj1b+aIe?2CsV0)t7=-`O*=h!JLS4Sqrys=yj zFY^;jNiXUoG&KheUA$! zmF)${0E3GMe? ziUXiU$+Abj-*f?1;jv|rSwA)y8QXi0bdl>t!g+U_R>uc3)q*?C+~b3T)fQt`A0PA? zyj~i?BnsqA6l+5H<5zGrWG}SVj(*rXTtFhX^b~X;V6HJGCj>)P?LB7tDM8X)dO~o# zs=rS>L(Od`1c$1v&Qmx2#9*;j)SiDzFhNz{FG-Fq-2Jp$t@kCrU_^$R6>P27S}y~dNAHsdzPBQ z@xeo?=Ki{;#|O8noOOa6&o412o&^QJb&0wDte`^GFEOjn3c9MACFcFJf{P@4%-Lv+ z{;jOPni$-oZoAj~&qNqG?l+O!@qFY`{KL*v#6qO&ewp^4k;0+;;c3#b+Gk3LV_lsbnxL5rm=-hGMYKJIc@nuQu z+o3#mUI${O`uHz`Q#;Okf&_327^4V7&L%m0FDu;rxJh0OkheW4hAAZFA?dqUU%iYWImxt?Xaqd;&ClA(@UlW|9ifit7<%Vink@kGW6|B1#oB2GB zqi`Rs1Rrq!r!qKO+#yxegnL#MY6Iu*T=(aypvaGJrh>Rtm&n14&C5;j%V2(<;4brM z+w!{K{xbN)mod9`u0x5Mxxskx|7)%r>$2+^AI53y^}(zA+lYR5L(tKLHv}sW7}Zs+ zdfarqF}N?9Q6=W18-sC?C$ay`GUw0ZhN#7lo4e=H!G+M@dBGevdtO~)j=Txv-MYj~ zyeVj_US49Zyoq%zvuF8D!KC7?Ofg7!OW)(Xxe(rxkol0fnZez-#LT}LEZM%e?sqo_ zqp}NMdfLyH)oX)Suym~H^IA|^*XB3D=6Ke_J{-|}zBH&mbjF1jPM&q%#EWKJdhwiz zv!_L0=l^`tB!Xv6zVOnECtWys@Y`!R`JQbNI60#=J*?TZ|mfn%9<5Bt z{YG>B13}-GjcwxIbYi6vc(>PRUU(oFmbbV#?)91+2^J@2&X_Sh@$=b6Lpq1~hG3TrbzVTO^Gae02RlSnt;YWjWxj4lXJr*37w=?O+`#Ur7 zv7lpKyvz;Vcjo%Xf}Z&UBJZd&^WGUNw9mL0{*p6{I~nWYvyQXgR=2wHXyc7 zdB7`Nr_7?ojmKx#GET< zP9B=rLwt#ipLX$#%P&sMzH;`Q$rmLi&zd!3R!O<3T7yn_K)G4G29h|g+>yJVm75Jz zlYcdS8AJTL)&zI8@6^l1Lt(s9ell@|ajY@-KE--)jd}H{V0Kcy z&Kpn>w?kZ0_t3Klq1jXL=PI+SJ{WHj4MD!TqOa-G5S*{N^)uBC!7*w|KeMF)<}<&a z+0zh==&+(++#5`JSM-g06L52*){t-9y5P9{SpT>@CPKVfwk|m3pb7ZHjPxVY>0N$7 zTImhx6VC-bvbK`Gt-0>@;LLm&`>ZPuo;BOsYaV$HI8Q$$?sX#RUh|jdf-an|&UxM< z&gkcZE}i!hDOivq-BTvco|3q5#w_T*S9++K`#h4gIrOgeLEGZhhdP8h>9WbQE}TB& z@}UWj(~!N_!@nAd5_b4c^V9V~mmaYJuF!uH40Z4l7tfeOp@>bMXEBtp0Rzn9^(+R* z4KN$mgKAX+%=Yzg#>M0n6k20Cz7TXdeG^_OdGzFqC(ru%1r9x_c^Ba#S)4NY0>l{D z+@0Oq}!c85gHw%MYuoej&KnZ@Fk-+{>YmrMQ;Am=-Su zzsm15C~ha+!6@SU#xShZw zg_pWUc>M;O>J8lR^EF5;crElIOU7@c@n1$!FmtdeeL3hnVheet&VKxM@y)P%Go~?^ zd<$qBsZ;RO(1iv3e#7rJez)+OH_%-1GGfH=A$2t`2bbxVXAh5iKO=E>e#I<*zcxKz z3zF*W;pVv4f>Xp@@)|Zh6Aq7i=aWSEnh?sA{~Er_{!iCd6WT-&g*Wv{Ye>m9sg?dn zV@Y#J+jy%OL7T+i{v~Y>wp6e}5R{@l*oqgmDyRoV1bqlSc&JbzU_@vti00tIi?$I# z+N9?95ETTqhzCpkUbfqY1Rn2w^XBc&&c2!XW|kD;N!UWWyr7VH1t1xx$45cWR=Dp2 zomWl!RYj=oXNX*eC!bS`S-F8?C(c1v!N60F0t08qJe@Ek`1(BUyOMxkR@z4pp-4KO zg>5Us5!mh$EDxsF{|9?_36{jt{j0FsOYkmX&Uyta`dZqp^@b=c~xZ^5`@?xIRpofLXJQVLk>X>LVTQ9 zB$wyqF)is^ZJ0`7OA^jOqk$WG1RDz5$R^IgFXO9FPfPMUOW2B2C(kX>#I`q`YN6*o z@t!zls1-gQCtq1Z7f;2hscEoFn-Ol~3i7Zz_TtvE$P9LIDo(GB9o-s{vv>1Mg3cPk zY+a_|T62$f@xO4(OcIBd#wH>+mqBIA&h+x!7jn0a_o|8w|Gt;P?p-OLh#GrlL0G(EEM^jrM0b=u_3iU<^m`aQa3gfp>K3~ Si`9#+Ylf_$mlq)~der~$g!_H~ delta 96763 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

o}ryJsNit$%zO6q5vF^|8}2x83bxmepr`NH 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 Yo8zQ9{lu3r@H3IZknUgtFD#S)16?H Promise; + migrateContract: ({ + contractAddr, + msg, + newCodeId + }: { + contractAddr: string; + msg: Binary; + newCodeId: number; + }, $fee?: number | StdFee | "auto", $memo?: string, $funds?: Coin[]) => Promise; } export class OraiswapFactoryClient extends OraiswapFactoryQueryClient implements OraiswapFactoryInterface { client: SigningCosmWasmClient; @@ -105,6 +114,7 @@ export class OraiswapFactoryClient extends OraiswapFactoryQueryClient implements this.updateConfig = this.updateConfig.bind(this); this.createPair = this.createPair.bind(this); this.addPair = this.addPair.bind(this); + this.migrateContract = this.migrateContract.bind(this); } updateConfig = async ({ @@ -149,4 +159,21 @@ export class OraiswapFactoryClient extends OraiswapFactoryQueryClient implements } }, $fee, $memo, $funds); }; + migrateContract = async ({ + contractAddr, + msg, + newCodeId + }: { + contractAddr: string; + msg: Binary; + newCodeId: number; + }, $fee: number | StdFee | "auto" = "auto", $memo?: string, $funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + migrate_contract: { + contract_addr: contractAddr, + msg, + new_code_id: newCodeId + } + }, $fee, $memo, $funds); + }; } \ No newline at end of file diff --git a/packages/contracts-sdk/src/OraiswapFactory.types.ts b/packages/contracts-sdk/src/OraiswapFactory.types.ts index 7597b056..af6dd7d3 100644 --- a/packages/contracts-sdk/src/OraiswapFactory.types.ts +++ b/packages/contracts-sdk/src/OraiswapFactory.types.ts @@ -1,4 +1,4 @@ -import {Addr, AssetInfo, PairInfo} from "./types"; +import {Addr, AssetInfo, Binary, PairInfo} from "./types"; export interface InstantiateMsg { commission_rate?: string | null; oracle_addr: Addr; @@ -20,6 +20,12 @@ export type ExecuteMsg = { add_pair: { pair_info: PairInfo; }; +} | { + migrate_contract: { + contract_addr: string; + msg: Binary; + new_code_id: number; + }; }; export type QueryMsg = { config: {}; diff --git a/packages/contracts-sdk/src/types.ts b/packages/contracts-sdk/src/types.ts index a9f6b9cf..2b1bc9df 100644 --- a/packages/contracts-sdk/src/types.ts +++ b/packages/contracts-sdk/src/types.ts @@ -31,11 +31,11 @@ export interface PairInfo { liquidity_token: Addr; oracle_addr: Addr; } -export type OrderDirection = "buy" | "sell"; export interface Asset { amount: Uint128; info: AssetInfo; } +export type OrderDirection = "buy" | "sell"; export type OrderFilter = ("tick" | "none") | { bidder: string; } | { diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index 2d8b4026..20bfe150 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -225,7 +225,7 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { } } return { - // transactions: txs, + transactions: txs, swapOpsData, accountTxs, provideLiquidityOpsData, diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index ac6488e6..57632d63 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -1,4 +1,5 @@ import { Log } from "@cosmjs/stargate/build/logs"; +import { Tx } from "@oraichain/cosmos-rpc-sync"; import { Addr, Asset, @@ -50,7 +51,7 @@ export type WithdrawLiquidityOperationData = { }; export type TxAnlysisResult = { - // transactions: Tx[]; + transactions: Tx[]; swapOpsData: SwapOperationData[]; accountTxs: AccountTx[]; provideLiquidityOpsData: ProvideLiquidityOperationData[]; From 34be66c8c43ba446668943022048245c032901d7 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 13 Jul 2023 10:17:39 +0700 Subject: [PATCH 06/75] added tx insert functions & refactored code --- packages/oraidex-sync/src/index.ts | 5 +++- packages/oraidex-sync/src/tx-insert.ts | 30 +++++++++++++++++++ .../src/{helper.ts => tx-parsing.ts} | 4 +-- packages/oraidex-sync/tests/helper.spec.ts | 24 +++++++++++---- 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 packages/oraidex-sync/src/tx-insert.ts rename packages/oraidex-sync/src/{helper.ts => tx-parsing.ts} (98%) diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 6871bc99..b689f83f 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -1,8 +1,9 @@ import "dotenv/config"; -import { parseTxs, parseWasmEvents } from "./helper"; +import { parseTxs } from "./tx-parsing"; import { DuckDb } from "./db"; import { WriteData, SyncData, Txs } from "@oraichain/cosmos-rpc-sync"; import "dotenv/config"; +import { insertParsedTxs } from "./tx-insert"; const duckDb = new DuckDb(); class WriteOrders extends WriteData { @@ -14,6 +15,8 @@ class WriteOrders extends WriteData { try { const { txs, offset: newOffset, queryTags } = chunk as Txs; const result = parseTxs(txs); + // insert txs + await insertParsedTxs(result); console.dir(result, { depth: null }); } catch (error) { console.log("error processing data: ", error); diff --git a/packages/oraidex-sync/src/tx-insert.ts b/packages/oraidex-sync/src/tx-insert.ts new file mode 100644 index 00000000..d7e3351f --- /dev/null +++ b/packages/oraidex-sync/src/tx-insert.ts @@ -0,0 +1,30 @@ +import { + ProvideLiquidityOperationData, + SwapOperationData, + TxAnlysisResult, + WithdrawLiquidityOperationData, +} from "./types"; + +async function insertSwapOps(ops: SwapOperationData[]) {} + +async function insertProvideLiquidityOps( + ops: ProvideLiquidityOperationData[] +) {} + +async function insertWithdrawLiquidityOps( + ops: WithdrawLiquidityOperationData[] +) {} + +async function insertParsedTxs(txs: TxAnlysisResult) { + // insert swap ops + const insertResults = await Promise.all([ + insertSwapOps(txs.swapOpsData), + insertProvideLiquidityOps(txs.provideLiquidityOpsData), + insertWithdrawLiquidityOps(txs.withdrawLiquidityOpsData), + ]); + console.log("insert results: ", insertResults); + // insert provide liquidity ops + // insert withdraw liquidity ops +} + +export { insertParsedTxs }; diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/tx-parsing.ts similarity index 98% rename from packages/oraidex-sync/src/helper.ts rename to packages/oraidex-sync/src/tx-parsing.ts index 20bfe150..1f301289 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -1,6 +1,6 @@ -import { Attribute, Event, IndexedTx } from "@cosmjs/stargate"; +import { Attribute, Event } from "@cosmjs/stargate"; import { Tx } from "@oraichain/cosmos-rpc-sync"; -import { Asset, AssetInfo } from "@oraichain/oraidex-contracts-sdk"; +import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; import { Tx as CosmosTx } from "cosmjs-types/cosmos/tx/v1beta1/tx"; import { diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index 9ba59a61..7895cf2a 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -1,9 +1,21 @@ -import * as helper from "../src/helper"; +import * as helper from "../src/tx-parsing"; -describe("test-helper", () => { - it("test-parseWithdrawLiquidityAssets", () => { - const result = helper.parseWithdrawLiquidityAssets( - "2591orai, 773ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78" - ); +describe("test-tx-parsing", () => { + it.each<[string, string[]]>([ + [ + "2591orai, 773ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78", + [ + "2591", + "orai", + "773", + "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78", + ], + ], + ["foo", []], + ])("test-parseWithdrawLiquidityAssets", (assets, expectedParsing) => { + // act + const result = helper.parseWithdrawLiquidityAssets(assets); + // assert + expect(result).toEqual(expectedParsing); }); }); From db4fffa2606b9708f0b8cebe7c2797b6006f9661 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 13 Jul 2023 11:09:02 +0700 Subject: [PATCH 07/75] added vscode prettier settings, created swap & lp sql commands --- .vscode/settings.json | 7 ++ packages/oraidex-sync/src/db.ts | 63 +++++++++++-- packages/oraidex-sync/src/tx-parsing.ts | 118 ++++++++++-------------- packages/oraidex-sync/src/types.ts | 41 +++----- 4 files changed, 120 insertions(+), 109 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..c0cb01f6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "prettier.singleQuote": true, + "prettier.trailingComma": "none", + "prettier.jsxSingleQuote": false, + "prettier.printWidth": 120, + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index aef419d6..beee3b50 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -1,4 +1,18 @@ -import { Database, Connection } from "duckdb-async"; +import { Database, Connection } from 'duckdb-async'; +import { ProvideLiquidityOperationData, SwapOperationData, WithdrawLiquidityOperationData } from './types'; +import fs from 'fs'; + +function generateRandomString(length) { + const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + let randomString = ''; + + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * letters.length); + randomString += letters[randomIndex]; + } + + return randomString; +} export class DuckDb { private db: Database; @@ -8,7 +22,7 @@ export class DuckDb { } async initDuckDb(fileName?: string): Promise { - this.db = await Database.create(fileName ?? "oraidex-sync-data"); + this.db = await Database.create(fileName ?? 'oraidex-sync-data'); } async initDuckDbConnection(): Promise { @@ -17,22 +31,53 @@ export class DuckDb { async createHeightSnapshot() { const db = await this.initDuckDbConnection(); - await db.all( - "CREATE TABLE IF NOT EXISTS height_snapshot (currentInd INTEGER,PRIMARY KEY (currentInd))" - ); + await db.exec('CREATE TABLE IF NOT EXISTS height_snapshot (currentInd INTEGER,PRIMARY KEY (currentInd))'); } async loadHeightSnapshot() { const db = await this.initDuckDbConnection(); - const result = await db.all("SELECT * FROM height_snapshot"); + const result = await db.all('SELECT * FROM height_snapshot'); return result.length > 0 ? result[0] : { currentInd: 1 }; } async insertHeightSnapshot(currentInd: number) { const db = await this.initDuckDbConnection(); - await db.all( - "INSERT OR REPLACE INTO height_snapshot VALUES (?)", - currentInd + await db.exec('INSERT OR REPLACE INTO height_snapshot VALUES (?)', currentInd); + } + + private async insertBulkData(data: any[], tableName: string) { + const db = await this.initDuckDbConnection(); + // random because we will discard this file once we finish inserting the bulk data into the db + const randomString = generateRandomString(20); + // const randomString = 'foobar'; + const randomFile = `${randomString}.json`; + await fs.promises.writeFile(randomFile, JSON.stringify(data)); + await db.exec(`CREATE TEMP TABLE ${randomString} AS SELECT * FROM read_json_auto(?)`, randomFile); + // delete file before inserting because the insertion may throw error + fs.unlink(randomFile, () => {}); + await db.exec(`INSERT INTO ${tableName} SELECT * FROM ${randomString}`); + } + + async createSwapOpsTable() { + const db = await this.initDuckDbConnection(); + await db.exec( + 'CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, offerDenom VARCHAR, offerAmount INTEGER, askDenom VARCHAR, returnAmount INTEGER, taxAmount INTEGER, commissionAmount INTEGER, spreadAmount INTEGER)' + ); + } + + async insertSwapOps(ops: SwapOperationData[]) { + await this.insertBulkData(ops, 'swap_ops_data'); + } + + async createLiquidityOpsTable() { + const db = await this.initDuckDbConnection(); + await db.exec('CREATE TYPE LPOPTYPE AS ENUM ("provide","withdraw")'); + await db.exec( + 'CREATE TABLE IF NOT EXISTS lp_ops_data (txhash VARCHAR, firstTokenAmount INTEGER, firstTokenDenom VARCHAR, secondTokenAmount INTEGER, secondTokenDenom VARCHAR, provider VARCHAR, opType LPOPTYPE)' ); } + + async insertLpOps(ops: WithdrawLiquidityOperationData[]) { + await this.insertBulkData(ops, 'lp_ops_data'); + } } diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 1f301289..4c59b807 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -1,8 +1,8 @@ -import { Attribute, Event } from "@cosmjs/stargate"; -import { Tx } from "@oraichain/cosmos-rpc-sync"; -import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; -import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; -import { Tx as CosmosTx } from "cosmjs-types/cosmos/tx/v1beta1/tx"; +import { Attribute, Event } from '@cosmjs/stargate'; +import { Tx } from '@oraichain/cosmos-rpc-sync'; +import { AssetInfo } from '@oraichain/oraidex-contracts-sdk'; +import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'; +import { Tx as CosmosTx } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; import { AccountTx, ModifiedMsgExecuteContract, @@ -12,12 +12,12 @@ import { ProvideLiquidityOperationData, SwapOperationData, TxAnlysisResult, - WithdrawLiquidityOperationData, -} from "./types"; -import { Log } from "@cosmjs/stargate/build/logs"; + WithdrawLiquidityOperationData +} from './types'; +import { Log } from '@cosmjs/stargate/build/logs'; function parseAssetInfo(info: AssetInfo): string { - if ("native_token" in info) return info.native_token.denom; + if ('native_token' in info) return info.native_token.denom; return info.token.contract_addr; } @@ -26,9 +26,7 @@ async function delay(timeout: number) { } function parseWasmEvents(events: readonly Event[]): (readonly Attribute[])[] { - return events - .filter((event) => event.type === "wasm") - .map((event) => event.attributes); + return events.filter((event) => event.type === 'wasm').map((event) => event.attributes); } function parseTxLog(rawLog: string): Log[] { @@ -43,7 +41,7 @@ function parseTxToMsgExecuteContractMsgs(tx: Tx): MsgExecuteContractWithLogs[] { const msgs: MsgExecuteContractWithLogs[] = []; for (let i = 0; i < cosmosTx.body.messages.length; i++) { const msg = cosmosTx.body.messages[i]; - if (msg.typeUrl === "/cosmwasm.wasm.v1.MsgExecuteContract") { + if (msg.typeUrl === '/cosmwasm.wasm.v1.MsgExecuteContract') { const msgExecuteContract = MsgExecuteContract.decode(msg.value); // TODO: this is an assumption that the log order is the same as the message order. msgs.push({ ...msgExecuteContract, logs: logs[i] }); @@ -52,10 +50,7 @@ function parseTxToMsgExecuteContractMsgs(tx: Tx): MsgExecuteContractWithLogs[] { return msgs; } -function extractSwapOperations( - txhash: string, - events: readonly Event[] -): SwapOperationData[] { +function extractSwapOperations(txhash: string, events: readonly Event[]): SwapOperationData[] { const wasmAttributes = parseWasmEvents(events); let swapData: SwapOperationData[] = []; let offerDenoms: string[] = []; @@ -66,27 +61,27 @@ function extractSwapOperations( let taxAmounts: string[] = []; let spreadAmounts: string[] = []; for (let attrs of wasmAttributes) { - if (!attrs.find((attr) => attr.key === "swap")) continue; + if (!attrs.find((attr) => attr.key === 'swap')) continue; for (let attr of attrs) { - if (attr.key === "offer_asset") { + if (attr.key === 'offer_asset') { offerDenoms.push(attr.value); } - if (attr.key === "ask_asset") { + if (attr.key === 'ask_asset') { askDenoms.push(attr.value); } - if (attr.key === "offer_amount") { + if (attr.key === 'offer_amount') { offerAmounts.push(attr.value); } - if (attr.key === "return_amount") { + if (attr.key === 'return_amount') { returnAmounts.push(attr.value); } - if (attr.key === "tax_amount") { + if (attr.key === 'tax_amount') { taxAmounts.push(attr.value); } - if (attr.key === "commission_amount") { + if (attr.key === 'commission_amount') { commissionAmounts.push(attr.value); } - if (attr.key === "spread_amount") { + if (attr.key === 'spread_amount') { spreadAmounts.push(attr.value); } } @@ -101,7 +96,7 @@ function extractSwapOperations( commissionAmount: parseInt(commissionAmounts[i]), spreadAmount: parseInt(spreadAmounts[i]), taxAmount: parseInt(taxAmounts[i]), - askDenom: askDenoms[i], + askDenom: askDenoms[i] }); } return swapData; @@ -110,14 +105,18 @@ function extractSwapOperations( function extractMsgProvideLiquidity( txhash: string, msg: MsgType, - provider: string + txCreator: string ): ProvideLiquidityOperationData | undefined { - if ("provide_liquidity" in msg) { + if ('provide_liquidity' in msg) { + const firstAsset = msg.provide_liquidity.assets[0]; + const secAsset = msg.provide_liquidity.assets[1]; return { txhash, - firstTokenAsset: msg.provide_liquidity.assets[0], - secondTokenAsset: msg.provide_liquidity.assets[1], - provider, + firstTokenAmount: parseInt(firstAsset.amount), + firstTokenDenom: parseAssetInfo(firstAsset.info), + secondTokenAmount: parseInt(secAsset.amount), + secondTokenDenom: parseAssetInfo(secAsset.info), + txCreator }; } return undefined; @@ -127,7 +126,6 @@ function parseWithdrawLiquidityAssets(assets: string): string[] { // format: "2591orai, 773ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78" const regex = /^(\d+)([a-zA-Z\/0-9]+), (\d+)([a-zA-Z\/0-9]+)/; const matches = assets.match(regex); - console.log("matches: ", matches); if (!matches || matches.length < 5) return []; // check < 5 because the string should be split into two numbers and two strings return matches.slice(1, 5); } @@ -135,56 +133,47 @@ function parseWithdrawLiquidityAssets(assets: string): string[] { function extractMsgWithdrawLiquidity( txhash: string, events: readonly Event[], - withdrawer: string + txCreator: string ): WithdrawLiquidityOperationData[] { const withdrawData: WithdrawLiquidityOperationData[] = []; const wasmAttributes = parseWasmEvents(events); for (let attrs of wasmAttributes) { - if (!attrs.find((attr) => attr.key === "withdraw_liquidity")) continue; - const assetAttr = attrs.find((attr) => attr.key === "refund_assets"); + if (!attrs.find((attr) => attr.key === 'withdraw_liquidity')) continue; + const assetAttr = attrs.find((attr) => attr.key === 'refund_assets'); if (!assetAttr) continue; const assets = parseWithdrawLiquidityAssets(assetAttr.value); // sanity check. only push data if can parse asset successfully if (assets.length !== 4) continue; withdrawData.push({ txhash, - firstTokenAmount: assets[0], + firstTokenAmount: parseInt(assets[0]), firstTokenDenom: assets[1], - secondTokenAmount: assets[2], + secondTokenAmount: parseInt(assets[2]), secondTokenDenom: assets[3], - withdrawer, + txCreator }); } return withdrawData; } -function parseExecuteContractToOraidexMsgs( - msgs: MsgExecuteContractWithLogs[] -): ModifiedMsgExecuteContract[] { +function parseExecuteContractToOraidexMsgs(msgs: MsgExecuteContractWithLogs[]): ModifiedMsgExecuteContract[] { let objs: ModifiedMsgExecuteContract[] = []; for (let msg of msgs) { try { const obj: ModifiedMsgExecuteContract = { ...msg, - msg: JSON.parse(Buffer.from(msg.msg).toString("utf-8")), + msg: JSON.parse(Buffer.from(msg.msg).toString('utf-8')) }; // Should be provide, remove liquidity, swap, or other oraidex related types - if ( - "provide_liquidity" in obj.msg || - "execute_swap_operations" in obj.msg || - "execute_swap_operation" in obj.msg - ) + if ('provide_liquidity' in obj.msg || 'execute_swap_operations' in obj.msg || 'execute_swap_operation' in obj.msg) objs.push(obj); - if ("send" in obj.msg) { + if ('send' in obj.msg) { try { const contractSendMsg: OraiswapPairCw20HookMsg = JSON.parse( - Buffer.from(obj.msg.send.msg, "base64").toString("utf-8") + Buffer.from(obj.msg.send.msg, 'base64').toString('utf-8') ); - if ( - "swap" in contractSendMsg || - "withdraw_liquidity" in contractSendMsg - ) + if ('swap' in contractSendMsg || 'withdraw_liquidity' in contractSendMsg) objs.push({ ...msg, msg: contractSendMsg }); } catch (error) { // do nothing because we dont care about other types of msgs @@ -211,16 +200,9 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { for (let msg of msgs) { const sender = msg.sender; swapOpsData.push(...extractSwapOperations(txhash, msg.logs.events)); - const provideLiquidityData = extractMsgProvideLiquidity( - txhash, - msg.msg, - sender - ); - if (provideLiquidityData) - provideLiquidityOpsData.push(provideLiquidityData); - withdrawLiquidityOpsData.push( - ...extractMsgWithdrawLiquidity(txhash, msg.logs.events, sender) - ); + const provideLiquidityData = extractMsgProvideLiquidity(txhash, msg.msg, sender); + if (provideLiquidityData) provideLiquidityOpsData.push(provideLiquidityData); + withdrawLiquidityOpsData.push(...extractMsgWithdrawLiquidity(txhash, msg.logs.events, sender)); accountTxs.push({ txhash, accountAddress: sender }); } } @@ -229,14 +211,8 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { swapOpsData, accountTxs, provideLiquidityOpsData, - withdrawLiquidityOpsData, + withdrawLiquidityOpsData }; } -export { - parseAssetInfo, - delay, - parseWasmEvents, - parseTxs, - parseWithdrawLiquidityAssets, -}; +export { parseAssetInfo, delay, parseWasmEvents, parseTxs, parseWithdrawLiquidityAssets }; diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 57632d63..70b67047 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -1,15 +1,8 @@ -import { Log } from "@cosmjs/stargate/build/logs"; -import { Tx } from "@oraichain/cosmos-rpc-sync"; -import { - Addr, - Asset, - AssetInfo, - Binary, - Decimal, - Uint128, -} from "@oraichain/oraidex-contracts-sdk"; -import { ExecuteMsg as OraiswapRouterExecuteMsg } from "@oraichain/oraidex-contracts-sdk/build/OraiswapRouter.types"; -import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; +import { Log } from '@cosmjs/stargate/build/logs'; +import { Tx } from '@oraichain/cosmos-rpc-sync'; +import { Addr, Asset, AssetInfo, Binary, Decimal, Uint128 } from '@oraichain/oraidex-contracts-sdk'; +import { ExecuteMsg as OraiswapRouterExecuteMsg } from '@oraichain/oraidex-contracts-sdk/build/OraiswapRouter.types'; +import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'; export type AssetData = { info: AssetInfo; @@ -36,20 +29,15 @@ export type AccountTx = { export type ProvideLiquidityOperationData = { txhash: string; - firstTokenAsset: Asset; - secondTokenAsset: Asset; - provider: string; -}; - -export type WithdrawLiquidityOperationData = { - txhash: string; + firstTokenAmount: number; firstTokenDenom: string; - firstTokenAmount: string; + secondTokenAmount: number; secondTokenDenom: string; - secondTokenAmount: string; - withdrawer: string; + txCreator: string; }; +export type WithdrawLiquidityOperationData = ProvideLiquidityOperationData; + export type TxAnlysisResult = { transactions: Tx[]; swapOpsData: SwapOperationData[]; @@ -62,10 +50,7 @@ export type MsgExecuteContractWithLogs = MsgExecuteContract & { logs: Log; }; -export type ModifiedMsgExecuteContract = Omit< - MsgExecuteContractWithLogs, - "msg" -> & { +export type ModifiedMsgExecuteContract = Omit & { msg: MsgType; }; @@ -88,9 +73,7 @@ export type MsgType = | OraiswapPairCw20HookMsg; export type OraiswapPairCw20HookMsg = { - swap: - | { belief_price?: Decimal; max_spread?: Decimal; to?: string } - | { withdraw_liquidity: {} }; + swap: { belief_price?: Decimal; max_spread?: Decimal; to?: string } | { withdraw_liquidity: {} }; }; export type PairMapping = { From 3b311f81faea7c9f7d239d7b7a13fa0150d66726 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 13 Jul 2023 14:14:18 +0700 Subject: [PATCH 08/75] finished inserting oraidex data & added test cases --- .vscode/settings.json | 2 +- package.json | 1 + packages/oraidex-sync/src/db.ts | 67 +++++++++++++------------ packages/oraidex-sync/src/index.ts | 13 +++-- packages/oraidex-sync/src/pairs.ts | 55 ++++++-------------- packages/oraidex-sync/src/tx-insert.ts | 27 +++++----- packages/oraidex-sync/src/tx-parsing.ts | 60 +++++++++++----------- packages/oraidex-sync/src/tx-query.ts | 11 ++++ packages/oraidex-sync/src/types.ts | 17 ++++--- packages/oraidex-sync/tests/db.spec.ts | 45 +++++++++++++++++ 10 files changed, 169 insertions(+), 129 deletions(-) create mode 100644 packages/oraidex-sync/src/tx-query.ts create mode 100644 packages/oraidex-sync/tests/db.spec.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index c0cb01f6..94373e02 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "prettier.singleQuote": true, + "prettier.singleQuote": false, "prettier.trailingComma": "none", "prettier.jsxSingleQuote": false, "prettier.printWidth": 120, diff --git a/package.json b/package.json index a16f7797..a955be0e 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "license": "MIT", "scripts": { "postinstall": "patch-package", + "test": "jest", "docs": "typedoc --entryPointStrategy expand --name 'Oraidex SDK' --readme none --tsconfig packages/contracts-sdk/tsconfig.json packages/contracts-sdk/src", "build": "tsc -p", "deploy": "yarn publish --access public" diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index beee3b50..f7a89458 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -1,18 +1,6 @@ -import { Database, Connection } from 'duckdb-async'; -import { ProvideLiquidityOperationData, SwapOperationData, WithdrawLiquidityOperationData } from './types'; -import fs from 'fs'; - -function generateRandomString(length) { - const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - let randomString = ''; - - for (let i = 0; i < length; i++) { - const randomIndex = Math.floor(Math.random() * letters.length); - randomString += letters[randomIndex]; - } - - return randomString; -} +import { Database, Connection } from "duckdb-async"; +import { SwapOperationData, WithdrawLiquidityOperationData } from "./types"; +import fs from "fs"; export class DuckDb { private db: Database; @@ -22,7 +10,7 @@ export class DuckDb { } async initDuckDb(fileName?: string): Promise { - this.db = await Database.create(fileName ?? 'oraidex-sync-data'); + this.db = await Database.create(fileName ?? "oraidex-sync-data"); } async initDuckDbConnection(): Promise { @@ -31,53 +19,66 @@ export class DuckDb { async createHeightSnapshot() { const db = await this.initDuckDbConnection(); - await db.exec('CREATE TABLE IF NOT EXISTS height_snapshot (currentInd INTEGER,PRIMARY KEY (currentInd))'); + await db.exec("CREATE TABLE IF NOT EXISTS height_snapshot (currentInd INTEGER,PRIMARY KEY (currentInd))"); } async loadHeightSnapshot() { const db = await this.initDuckDbConnection(); - const result = await db.all('SELECT * FROM height_snapshot'); + const result = await db.all("SELECT * FROM height_snapshot"); return result.length > 0 ? result[0] : { currentInd: 1 }; } async insertHeightSnapshot(currentInd: number) { const db = await this.initDuckDbConnection(); - await db.exec('INSERT OR REPLACE INTO height_snapshot VALUES (?)', currentInd); + await db.exec("INSERT OR REPLACE INTO height_snapshot VALUES (?)", currentInd); } private async insertBulkData(data: any[], tableName: string) { + // we wont insert anything if the data is empty. Otherwise it would throw an error while inserting + if (data.length === 0) return; const db = await this.initDuckDbConnection(); - // random because we will discard this file once we finish inserting the bulk data into the db - const randomString = generateRandomString(20); - // const randomString = 'foobar'; - const randomFile = `${randomString}.json`; - await fs.promises.writeFile(randomFile, JSON.stringify(data)); - await db.exec(`CREATE TEMP TABLE ${randomString} AS SELECT * FROM read_json_auto(?)`, randomFile); - // delete file before inserting because the insertion may throw error - fs.unlink(randomFile, () => {}); - await db.exec(`INSERT INTO ${tableName} SELECT * FROM ${randomString}`); + const tableFile = `${tableName}.json`; + // the file written out is temporary only. Will be deleted after insertion + await fs.promises.writeFile(tableFile, JSON.stringify(data)); + await db.run(`INSERT INTO ${tableName} SELECT * FROM read_json_auto(?)`, tableFile); + await fs.promises.unlink(tableFile); } async createSwapOpsTable() { const db = await this.initDuckDbConnection(); await db.exec( - 'CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, offerDenom VARCHAR, offerAmount INTEGER, askDenom VARCHAR, returnAmount INTEGER, taxAmount INTEGER, commissionAmount INTEGER, spreadAmount INTEGER)' + "CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, offerDenom VARCHAR, offerAmount INTEGER, askDenom VARCHAR, returnAmount INTEGER, taxAmount INTEGER, commissionAmount INTEGER, spreadAmount INTEGER)" ); } async insertSwapOps(ops: SwapOperationData[]) { - await this.insertBulkData(ops, 'swap_ops_data'); + await this.insertBulkData(ops, "swap_ops_data"); } async createLiquidityOpsTable() { const db = await this.initDuckDbConnection(); - await db.exec('CREATE TYPE LPOPTYPE AS ENUM ("provide","withdraw")'); + try { + await db.all("select enum_range(NULL::LPOPTYPE);"); + } catch (error) { + // if error it means the enum does not exist => create new + await db.exec("CREATE TYPE LPOPTYPE AS ENUM ('provide','withdraw');"); + } await db.exec( - 'CREATE TABLE IF NOT EXISTS lp_ops_data (txhash VARCHAR, firstTokenAmount INTEGER, firstTokenDenom VARCHAR, secondTokenAmount INTEGER, secondTokenDenom VARCHAR, provider VARCHAR, opType LPOPTYPE)' + "CREATE TABLE IF NOT EXISTS lp_ops_data (txhash VARCHAR, firstTokenAmount INTEGER, firstTokenDenom VARCHAR, secondTokenAmount INTEGER, secondTokenDenom VARCHAR, txCreator VARCHAR, opType LPOPTYPE)" ); } async insertLpOps(ops: WithdrawLiquidityOperationData[]) { - await this.insertBulkData(ops, 'lp_ops_data'); + await this.insertBulkData(ops, "lp_ops_data"); + } + + async querySwapOps() { + const db = await this.initDuckDbConnection(); + return db.all("SELECT * from swap_ops_data limit 1"); + } + + async queryLpOps() { + const db = await this.initDuckDbConnection(); + return db.all("SELECT * from lp_ops_data limit 1"); } } diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index b689f83f..277fc0fa 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -4,6 +4,7 @@ import { DuckDb } from "./db"; import { WriteData, SyncData, Txs } from "@oraichain/cosmos-rpc-sync"; import "dotenv/config"; import { insertParsedTxs } from "./tx-insert"; +import { queryLpOps, querySwapOps } from "./tx-query"; const duckDb = new DuckDb(); class WriteOrders extends WriteData { @@ -16,8 +17,14 @@ class WriteOrders extends WriteData { const { txs, offset: newOffset, queryTags } = chunk as Txs; const result = parseTxs(txs); // insert txs + console.log("result: ", result); await insertParsedTxs(result); - console.dir(result, { depth: null }); + + const swapOps = await querySwapOps(); + const lpOps = await queryLpOps(); + + console.log("swap ops: ", swapOps); + console.log("lp ops: ", lpOps); } catch (error) { console.log("error processing data: ", error); return false; @@ -29,7 +36,7 @@ class WriteOrders extends WriteData { const sync = async () => { try { await duckDb.initDuckDb(); - await duckDb.createHeightSnapshot(); + await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); const rpcUrl = process.env.RPC_URL || "https://rpc.orai.io"; let { currentInd } = await duckDb.loadHeightSnapshot(); // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic @@ -42,7 +49,7 @@ const sync = async () => { queryTags: [], limit: 100, maxThreadLevel: 1, - interval: 5000, + interval: 5000 }).pipe(new WriteOrders()); } catch (error) { console.log("error in start: ", error); diff --git a/packages/oraidex-sync/src/pairs.ts b/packages/oraidex-sync/src/pairs.ts index de80754d..3846b9e5 100644 --- a/packages/oraidex-sync/src/pairs.ts +++ b/packages/oraidex-sync/src/pairs.ts @@ -11,71 +11,44 @@ import { scOraiCw20Address, tronCw20Address, usdcCw20Address, - usdtCw20Address, + usdtCw20Address } from "./constants"; import { PairMapping } from "./types"; export const pairs: PairMapping[] = [ { - asset_infos: [ - { native_token: { denom: ORAI } }, - { token: { contract_addr: airiCw20Adress } }, - ], + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: airiCw20Adress } }] }, { - asset_infos: [ - { native_token: { denom: ORAI } }, - { token: { contract_addr: oraixCw20Address } }, - ], + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: oraixCw20Address } }] }, { - asset_infos: [ - { native_token: { denom: ORAI } }, - { token: { contract_addr: scOraiCw20Address } }, - ], + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: scOraiCw20Address } }] }, { - asset_infos: [ - { native_token: { denom: ORAI } }, - { native_token: { denom: atomIbcDenom } }, - ], + asset_infos: [{ native_token: { denom: ORAI } }, { native_token: { denom: atomIbcDenom } }] }, { - asset_infos: [ - { native_token: { denom: ORAI } }, - { token: { contract_addr: usdtCw20Address } }, - ], + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdtCw20Address } }] }, { - asset_infos: [ - { native_token: { denom: ORAI } }, - { token: { contract_addr: kwtCw20Address } }, - ], + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: kwtCw20Address } }] }, { asset_infos: [ { native_token: { denom: ORAI } }, { - native_token: { denom: osmosisIbcDenom }, - }, - ], + native_token: { denom: osmosisIbcDenom } + } + ] }, { - asset_infos: [ - { token: { contract_addr: milkyCw20Address } }, - { token: { contract_addr: usdtCw20Address } }, - ], + asset_infos: [{ token: { contract_addr: milkyCw20Address } }, { token: { contract_addr: usdtCw20Address } }] }, { - asset_infos: [ - { native_token: { denom: ORAI } }, - { token: { contract_addr: usdcCw20Address } }, - ], + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdcCw20Address } }] }, { - asset_infos: [ - { native_token: { denom: ORAI } }, - { token: { contract_addr: tronCw20Address } }, - ], - }, + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: tronCw20Address } }] + } ]; diff --git a/packages/oraidex-sync/src/tx-insert.ts b/packages/oraidex-sync/src/tx-insert.ts index d7e3351f..8dedf122 100644 --- a/packages/oraidex-sync/src/tx-insert.ts +++ b/packages/oraidex-sync/src/tx-insert.ts @@ -1,30 +1,27 @@ +import { duckDb } from "../src"; import { ProvideLiquidityOperationData, SwapOperationData, TxAnlysisResult, - WithdrawLiquidityOperationData, + WithdrawLiquidityOperationData } from "./types"; -async function insertSwapOps(ops: SwapOperationData[]) {} - -async function insertProvideLiquidityOps( - ops: ProvideLiquidityOperationData[] -) {} +async function insertSwapOps(ops: SwapOperationData[]) { + await duckDb.insertSwapOps(ops); +} -async function insertWithdrawLiquidityOps( - ops: WithdrawLiquidityOperationData[] -) {} +async function insertLiquidityOps(ops: ProvideLiquidityOperationData[] | WithdrawLiquidityOperationData[]) { + await duckDb.insertLpOps(ops); +} async function insertParsedTxs(txs: TxAnlysisResult) { // insert swap ops - const insertResults = await Promise.all([ + const results = await Promise.all([ insertSwapOps(txs.swapOpsData), - insertProvideLiquidityOps(txs.provideLiquidityOpsData), - insertWithdrawLiquidityOps(txs.withdrawLiquidityOpsData), + insertLiquidityOps(txs.provideLiquidityOpsData), + insertLiquidityOps(txs.withdrawLiquidityOpsData) ]); - console.log("insert results: ", insertResults); - // insert provide liquidity ops - // insert withdraw liquidity ops + console.log("insert results: ", results); } export { insertParsedTxs }; diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 4c59b807..9d74cf26 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -1,8 +1,8 @@ -import { Attribute, Event } from '@cosmjs/stargate'; -import { Tx } from '@oraichain/cosmos-rpc-sync'; -import { AssetInfo } from '@oraichain/oraidex-contracts-sdk'; -import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'; -import { Tx as CosmosTx } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; +import { Attribute, Event } from "@cosmjs/stargate"; +import { Tx } from "@oraichain/cosmos-rpc-sync"; +import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; +import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; +import { Tx as CosmosTx } from "cosmjs-types/cosmos/tx/v1beta1/tx"; import { AccountTx, ModifiedMsgExecuteContract, @@ -13,11 +13,11 @@ import { SwapOperationData, TxAnlysisResult, WithdrawLiquidityOperationData -} from './types'; -import { Log } from '@cosmjs/stargate/build/logs'; +} from "./types"; +import { Log } from "@cosmjs/stargate/build/logs"; function parseAssetInfo(info: AssetInfo): string { - if ('native_token' in info) return info.native_token.denom; + if ("native_token" in info) return info.native_token.denom; return info.token.contract_addr; } @@ -26,7 +26,7 @@ async function delay(timeout: number) { } function parseWasmEvents(events: readonly Event[]): (readonly Attribute[])[] { - return events.filter((event) => event.type === 'wasm').map((event) => event.attributes); + return events.filter((event) => event.type === "wasm").map((event) => event.attributes); } function parseTxLog(rawLog: string): Log[] { @@ -41,7 +41,7 @@ function parseTxToMsgExecuteContractMsgs(tx: Tx): MsgExecuteContractWithLogs[] { const msgs: MsgExecuteContractWithLogs[] = []; for (let i = 0; i < cosmosTx.body.messages.length; i++) { const msg = cosmosTx.body.messages[i]; - if (msg.typeUrl === '/cosmwasm.wasm.v1.MsgExecuteContract') { + if (msg.typeUrl === "/cosmwasm.wasm.v1.MsgExecuteContract") { const msgExecuteContract = MsgExecuteContract.decode(msg.value); // TODO: this is an assumption that the log order is the same as the message order. msgs.push({ ...msgExecuteContract, logs: logs[i] }); @@ -61,27 +61,27 @@ function extractSwapOperations(txhash: string, events: readonly Event[]): SwapOp let taxAmounts: string[] = []; let spreadAmounts: string[] = []; for (let attrs of wasmAttributes) { - if (!attrs.find((attr) => attr.key === 'swap')) continue; + if (!attrs.find((attr) => attr.key === "swap")) continue; for (let attr of attrs) { - if (attr.key === 'offer_asset') { + if (attr.key === "offer_asset") { offerDenoms.push(attr.value); } - if (attr.key === 'ask_asset') { + if (attr.key === "ask_asset") { askDenoms.push(attr.value); } - if (attr.key === 'offer_amount') { + if (attr.key === "offer_amount") { offerAmounts.push(attr.value); } - if (attr.key === 'return_amount') { + if (attr.key === "return_amount") { returnAmounts.push(attr.value); } - if (attr.key === 'tax_amount') { + if (attr.key === "tax_amount") { taxAmounts.push(attr.value); } - if (attr.key === 'commission_amount') { + if (attr.key === "commission_amount") { commissionAmounts.push(attr.value); } - if (attr.key === 'spread_amount') { + if (attr.key === "spread_amount") { spreadAmounts.push(attr.value); } } @@ -107,7 +107,7 @@ function extractMsgProvideLiquidity( msg: MsgType, txCreator: string ): ProvideLiquidityOperationData | undefined { - if ('provide_liquidity' in msg) { + if ("provide_liquidity" in msg) { const firstAsset = msg.provide_liquidity.assets[0]; const secAsset = msg.provide_liquidity.assets[1]; return { @@ -116,7 +116,8 @@ function extractMsgProvideLiquidity( firstTokenDenom: parseAssetInfo(firstAsset.info), secondTokenAmount: parseInt(secAsset.amount), secondTokenDenom: parseAssetInfo(secAsset.info), - txCreator + txCreator, + opType: "provide" }; } return undefined; @@ -139,8 +140,8 @@ function extractMsgWithdrawLiquidity( const wasmAttributes = parseWasmEvents(events); for (let attrs of wasmAttributes) { - if (!attrs.find((attr) => attr.key === 'withdraw_liquidity')) continue; - const assetAttr = attrs.find((attr) => attr.key === 'refund_assets'); + if (!attrs.find((attr) => attr.key === "withdraw_liquidity")) continue; + const assetAttr = attrs.find((attr) => attr.key === "refund_assets"); if (!assetAttr) continue; const assets = parseWithdrawLiquidityAssets(assetAttr.value); // sanity check. only push data if can parse asset successfully @@ -151,7 +152,8 @@ function extractMsgWithdrawLiquidity( firstTokenDenom: assets[1], secondTokenAmount: parseInt(assets[2]), secondTokenDenom: assets[3], - txCreator + txCreator, + opType: "withdraw" }); } return withdrawData; @@ -163,17 +165,17 @@ function parseExecuteContractToOraidexMsgs(msgs: MsgExecuteContractWithLogs[]): try { const obj: ModifiedMsgExecuteContract = { ...msg, - msg: JSON.parse(Buffer.from(msg.msg).toString('utf-8')) + msg: JSON.parse(Buffer.from(msg.msg).toString("utf-8")) }; // Should be provide, remove liquidity, swap, or other oraidex related types - if ('provide_liquidity' in obj.msg || 'execute_swap_operations' in obj.msg || 'execute_swap_operation' in obj.msg) + if ("provide_liquidity" in obj.msg || "execute_swap_operations" in obj.msg || "execute_swap_operation" in obj.msg) objs.push(obj); - if ('send' in obj.msg) { + if ("send" in obj.msg) { try { const contractSendMsg: OraiswapPairCw20HookMsg = JSON.parse( - Buffer.from(obj.msg.send.msg, 'base64').toString('utf-8') + Buffer.from(obj.msg.send.msg, "base64").toString("utf-8") ); - if ('swap' in contractSendMsg || 'withdraw_liquidity' in contractSendMsg) + if ("swap" in contractSendMsg || "withdraw_liquidity" in contractSendMsg) objs.push({ ...msg, msg: contractSendMsg }); } catch (error) { // do nothing because we dont care about other types of msgs @@ -207,7 +209,7 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { } } return { - transactions: txs, + // transactions: txs, swapOpsData, accountTxs, provideLiquidityOpsData, diff --git a/packages/oraidex-sync/src/tx-query.ts b/packages/oraidex-sync/src/tx-query.ts new file mode 100644 index 00000000..f52aa6c3 --- /dev/null +++ b/packages/oraidex-sync/src/tx-query.ts @@ -0,0 +1,11 @@ +import { duckDb } from "."; + +async function querySwapOps() { + return duckDb.querySwapOps(); +} + +async function queryLpOps() { + return duckDb.queryLpOps(); +} + +export { queryLpOps, querySwapOps }; diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 70b67047..0fe2b15e 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -1,8 +1,8 @@ -import { Log } from '@cosmjs/stargate/build/logs'; -import { Tx } from '@oraichain/cosmos-rpc-sync'; -import { Addr, Asset, AssetInfo, Binary, Decimal, Uint128 } from '@oraichain/oraidex-contracts-sdk'; -import { ExecuteMsg as OraiswapRouterExecuteMsg } from '@oraichain/oraidex-contracts-sdk/build/OraiswapRouter.types'; -import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'; +import { Log } from "@cosmjs/stargate/build/logs"; +import { Tx } from "@oraichain/cosmos-rpc-sync"; +import { Addr, Asset, AssetInfo, Binary, Decimal, Uint128 } from "@oraichain/oraidex-contracts-sdk"; +import { ExecuteMsg as OraiswapRouterExecuteMsg } from "@oraichain/oraidex-contracts-sdk/build/OraiswapRouter.types"; +import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; export type AssetData = { info: AssetInfo; @@ -27,6 +27,8 @@ export type AccountTx = { txhash: string; }; +export type LiquidityOpType = "provide" | "withdraw"; + export type ProvideLiquidityOperationData = { txhash: string; firstTokenAmount: number; @@ -34,12 +36,13 @@ export type ProvideLiquidityOperationData = { secondTokenAmount: number; secondTokenDenom: string; txCreator: string; + opType: LiquidityOpType; }; export type WithdrawLiquidityOperationData = ProvideLiquidityOperationData; export type TxAnlysisResult = { - transactions: Tx[]; + // transactions: Tx[]; swapOpsData: SwapOperationData[]; accountTxs: AccountTx[]; provideLiquidityOpsData: ProvideLiquidityOperationData[]; @@ -50,7 +53,7 @@ export type MsgExecuteContractWithLogs = MsgExecuteContract & { logs: Log; }; -export type ModifiedMsgExecuteContract = Omit & { +export type ModifiedMsgExecuteContract = Omit & { msg: MsgType; }; diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts new file mode 100644 index 00000000..509afdd3 --- /dev/null +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -0,0 +1,45 @@ +import { DuckDb } from "../src/db"; + +describe("test-duckdb", () => { + let duckDb: DuckDb; + beforeEach(async () => { + // fixture + duckDb = new DuckDb(); + await duckDb.initDuckDb(); + await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); + }); + + it("test-duckdb-insert-bulk-should-throw-error-when-wrong-data", async () => { + // act & test + await expect( + duckDb.insertLpOps([ + { + txhash: "foo", + firstTokenAmount: "abcd" as any, + firstTokenDenom: "orai", + secondTokenAmount: 2, + secondTokenDenom: "atom", + txCreator: "foobar", + opType: "provide" + } + ]) + ).rejects.toThrow(); + }); + + it("test-duckdb-insert-bulk-should-pass-and-can-query", async () => { + // act & test + await duckDb.insertLpOps([ + { + txhash: "foo", + firstTokenAmount: 1, + firstTokenDenom: "orai", + secondTokenAmount: 2, + secondTokenDenom: "atom", + txCreator: "foobar", + opType: "withdraw" + } + ]); + const queryResult = await duckDb.queryLpOps(); + console.log("query result: ", queryResult); + }); +}); From 2f435525197f5e13e31e11cc781bf2e34e9cd04d Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 13 Jul 2023 14:31:02 +0700 Subject: [PATCH 09/75] switched to uint and ubigint to fix overflow --- lp_ops_data.json | 1 + packages/oraidex-sync/src/db.ts | 12 ++++++------ packages/oraidex-sync/src/index.ts | 2 +- packages/oraidex-sync/src/tx-insert.ts | 3 +-- 4 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 lp_ops_data.json diff --git a/lp_ops_data.json b/lp_ops_data.json new file mode 100644 index 00000000..a1062f46 --- /dev/null +++ b/lp_ops_data.json @@ -0,0 +1 @@ +[{"txhash":"FEAA50A05E22F892DF72EA7C97A1777A915B75F2C9EBE923817ECEBDF5858B66","firstTokenAmount":2326916708,"firstTokenDenom":"orai1lus0f0rhx8s03gdllx2n6vhkmf0536dv57wfge","secondTokenAmount":6916711,"secondTokenDenom":"orai","txCreator":"orai129xc6z8d94arkglv6xqgq7du24m9d45fpv27lm","opType":"provide"}] \ No newline at end of file diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index f7a89458..7cdf37b3 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -19,7 +19,7 @@ export class DuckDb { async createHeightSnapshot() { const db = await this.initDuckDbConnection(); - await db.exec("CREATE TABLE IF NOT EXISTS height_snapshot (currentInd INTEGER,PRIMARY KEY (currentInd))"); + await db.exec("CREATE TABLE IF NOT EXISTS height_snapshot (currentInd UINTEGER,PRIMARY KEY (currentInd))"); } async loadHeightSnapshot() { @@ -30,7 +30,7 @@ export class DuckDb { async insertHeightSnapshot(currentInd: number) { const db = await this.initDuckDbConnection(); - await db.exec("INSERT OR REPLACE INTO height_snapshot VALUES (?)", currentInd); + await db.run("INSERT OR REPLACE INTO height_snapshot VALUES (?)", currentInd); } private async insertBulkData(data: any[], tableName: string) { @@ -47,7 +47,7 @@ export class DuckDb { async createSwapOpsTable() { const db = await this.initDuckDbConnection(); await db.exec( - "CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, offerDenom VARCHAR, offerAmount INTEGER, askDenom VARCHAR, returnAmount INTEGER, taxAmount INTEGER, commissionAmount INTEGER, spreadAmount INTEGER)" + "CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, offerDenom VARCHAR, offerAmount UBIGINT, askDenom VARCHAR, returnAmount UBIGINT, taxAmount UBIGINT, commissionAmount UBIGINT, spreadAmount UBIGINT)" ); } @@ -64,7 +64,7 @@ export class DuckDb { await db.exec("CREATE TYPE LPOPTYPE AS ENUM ('provide','withdraw');"); } await db.exec( - "CREATE TABLE IF NOT EXISTS lp_ops_data (txhash VARCHAR, firstTokenAmount INTEGER, firstTokenDenom VARCHAR, secondTokenAmount INTEGER, secondTokenDenom VARCHAR, txCreator VARCHAR, opType LPOPTYPE)" + "CREATE TABLE IF NOT EXISTS lp_ops_data (txhash VARCHAR, firstTokenAmount UBIGINT, firstTokenDenom VARCHAR, secondTokenAmount UBIGINT, secondTokenDenom VARCHAR, txCreator VARCHAR, opType LPOPTYPE)" ); } @@ -74,11 +74,11 @@ export class DuckDb { async querySwapOps() { const db = await this.initDuckDbConnection(); - return db.all("SELECT * from swap_ops_data limit 1"); + return db.all("SELECT count(*) from swap_ops_data"); } async queryLpOps() { const db = await this.initDuckDbConnection(); - return db.all("SELECT * from lp_ops_data limit 1"); + return db.all("SELECT count(*) from lp_ops_data"); } } diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 277fc0fa..389c050d 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -17,7 +17,7 @@ class WriteOrders extends WriteData { const { txs, offset: newOffset, queryTags } = chunk as Txs; const result = parseTxs(txs); // insert txs - console.log("result: ", result); + await duckDb.insertHeightSnapshot(newOffset); await insertParsedTxs(result); const swapOps = await querySwapOps(); diff --git a/packages/oraidex-sync/src/tx-insert.ts b/packages/oraidex-sync/src/tx-insert.ts index 8dedf122..9a9840c4 100644 --- a/packages/oraidex-sync/src/tx-insert.ts +++ b/packages/oraidex-sync/src/tx-insert.ts @@ -16,12 +16,11 @@ async function insertLiquidityOps(ops: ProvideLiquidityOperationData[] | Withdra async function insertParsedTxs(txs: TxAnlysisResult) { // insert swap ops - const results = await Promise.all([ + await Promise.all([ insertSwapOps(txs.swapOpsData), insertLiquidityOps(txs.provideLiquidityOpsData), insertLiquidityOps(txs.withdrawLiquidityOpsData) ]); - console.log("insert results: ", results); } export { insertParsedTxs }; From c3e5522ae6a3b9a869a32027062495000b5cbf0c Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 13 Jul 2023 14:31:30 +0700 Subject: [PATCH 10/75] removed lp op data json --- lp_ops_data.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 lp_ops_data.json diff --git a/lp_ops_data.json b/lp_ops_data.json deleted file mode 100644 index a1062f46..00000000 --- a/lp_ops_data.json +++ /dev/null @@ -1 +0,0 @@ -[{"txhash":"FEAA50A05E22F892DF72EA7C97A1777A915B75F2C9EBE923817ECEBDF5858B66","firstTokenAmount":2326916708,"firstTokenDenom":"orai1lus0f0rhx8s03gdllx2n6vhkmf0536dv57wfge","secondTokenAmount":6916711,"secondTokenDenom":"orai","txCreator":"orai129xc6z8d94arkglv6xqgq7du24m9d45fpv27lm","opType":"provide"}] \ No newline at end of file From 98d82524fed764f534dc0398eb69b3aa54d06247 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 13 Jul 2023 15:16:37 +0700 Subject: [PATCH 11/75] fixed parsing swap ops --- packages/oraidex-sync/src/index.ts | 1 + packages/oraidex-sync/src/tx-parsing.ts | 15 +++++++++------ packages/oraidex-sync/src/types.ts | 14 +++++++++++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 389c050d..94e11ee7 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -15,6 +15,7 @@ class WriteOrders extends WriteData { async process(chunk: any): Promise { try { const { txs, offset: newOffset, queryTags } = chunk as Txs; + console.log("new offset: ", newOffset); const result = parseTxs(txs); // insert txs await duckDb.insertHeightSnapshot(newOffset); diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 9d74cf26..a124c307 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -9,6 +9,7 @@ import { MsgExecuteContractWithLogs, MsgType, OraiswapPairCw20HookMsg, + OraiswapRouterCw20HookMsg, ProvideLiquidityOperationData, SwapOperationData, TxAnlysisResult, @@ -61,7 +62,7 @@ function extractSwapOperations(txhash: string, events: readonly Event[]): SwapOp let taxAmounts: string[] = []; let spreadAmounts: string[] = []; for (let attrs of wasmAttributes) { - if (!attrs.find((attr) => attr.key === "swap")) continue; + if (!attrs.find((attr) => attr.key === "action" && attr.value === "swap")) continue; for (let attr of attrs) { if (attr.key === "offer_asset") { offerDenoms.push(attr.value); @@ -92,11 +93,11 @@ function extractSwapOperations(txhash: string, events: readonly Event[]): SwapOp txhash, offerDenom: offerDenoms[i], offerAmount: offerAmounts[i], + askDenom: askDenoms[i], returnAmount: returnAmounts[i], - commissionAmount: parseInt(commissionAmounts[i]), - spreadAmount: parseInt(spreadAmounts[i]), taxAmount: parseInt(taxAmounts[i]), - askDenom: askDenoms[i] + commissionAmount: parseInt(commissionAmounts[i]), + spreadAmount: parseInt(spreadAmounts[i]) }); } return swapData; @@ -172,12 +173,14 @@ function parseExecuteContractToOraidexMsgs(msgs: MsgExecuteContractWithLogs[]): objs.push(obj); if ("send" in obj.msg) { try { - const contractSendMsg: OraiswapPairCw20HookMsg = JSON.parse( + const contractSendMsg: OraiswapPairCw20HookMsg | OraiswapRouterCw20HookMsg = JSON.parse( Buffer.from(obj.msg.send.msg, "base64").toString("utf-8") ); - if ("swap" in contractSendMsg || "withdraw_liquidity" in contractSendMsg) + if ("execute_swap_operations" in contractSendMsg || "withdraw_liquidity" in contractSendMsg) { objs.push({ ...msg, msg: contractSendMsg }); + } } catch (error) { + console.log("parsing msg send error: ", error); // do nothing because we dont care about other types of msgs } } diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 0fe2b15e..1b0194d1 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -1,6 +1,6 @@ import { Log } from "@cosmjs/stargate/build/logs"; import { Tx } from "@oraichain/cosmos-rpc-sync"; -import { Addr, Asset, AssetInfo, Binary, Decimal, Uint128 } from "@oraichain/oraidex-contracts-sdk"; +import { Addr, Asset, AssetInfo, Binary, Decimal, SwapOperation, Uint128 } from "@oraichain/oraidex-contracts-sdk"; import { ExecuteMsg as OraiswapRouterExecuteMsg } from "@oraichain/oraidex-contracts-sdk/build/OraiswapRouter.types"; import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; @@ -73,12 +73,20 @@ export type MsgType = msg: Binary; }; } + | OraiswapRouterCw20HookMsg | OraiswapPairCw20HookMsg; -export type OraiswapPairCw20HookMsg = { - swap: { belief_price?: Decimal; max_spread?: Decimal; to?: string } | { withdraw_liquidity: {} }; +export type OraiswapRouterCw20HookMsg = { + execute_swap_operations: { + minimum_receive?: Uint128 | null; + operations: SwapOperation[]; + to?: Addr | null; + }; }; +export type OraiswapPairCw20HookMsg = { + withdraw_liquidity: {}; +}; export type PairMapping = { asset_infos: [AssetInfo, AssetInfo]; }; From 7c21dc55498ea068f039c1041837af06dbf1510c Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 13 Jul 2023 15:37:27 +0700 Subject: [PATCH 12/75] added parseTxToMsgExecuteContractMsgs test cases --- packages/oraidex-sync/src/tx-parsing.ts | 9 +- packages/oraidex-sync/tests/helper.spec.ts | 21 ---- .../oraidex-sync/tests/tx-parsing.spec.ts | 97 +++++++++++++++++++ 3 files changed, 105 insertions(+), 22 deletions(-) delete mode 100644 packages/oraidex-sync/tests/helper.spec.ts create mode 100644 packages/oraidex-sync/tests/tx-parsing.spec.ts diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index a124c307..fa581874 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -220,4 +220,11 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { }; } -export { parseAssetInfo, delay, parseWasmEvents, parseTxs, parseWithdrawLiquidityAssets }; +export { + parseAssetInfo, + delay, + parseWasmEvents, + parseTxs, + parseWithdrawLiquidityAssets, + parseTxToMsgExecuteContractMsgs +}; diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts deleted file mode 100644 index 7895cf2a..00000000 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as helper from "../src/tx-parsing"; - -describe("test-tx-parsing", () => { - it.each<[string, string[]]>([ - [ - "2591orai, 773ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78", - [ - "2591", - "orai", - "773", - "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78", - ], - ], - ["foo", []], - ])("test-parseWithdrawLiquidityAssets", (assets, expectedParsing) => { - // act - const result = helper.parseWithdrawLiquidityAssets(assets); - // assert - expect(result).toEqual(expectedParsing); - }); -}); diff --git a/packages/oraidex-sync/tests/tx-parsing.spec.ts b/packages/oraidex-sync/tests/tx-parsing.spec.ts new file mode 100644 index 00000000..a183bb98 --- /dev/null +++ b/packages/oraidex-sync/tests/tx-parsing.spec.ts @@ -0,0 +1,97 @@ +import * as helper from "../src/tx-parsing"; +import { Tx } from "@oraichain/cosmos-rpc-sync"; +import { parseTxToMsgExecuteContractMsgs } from "../src/tx-parsing"; +import { Tx as CosmosTx } from "cosmjs-types/cosmos/tx/v1beta1/tx"; + +describe("test-tx-parsing", () => { + it.each<[string, string[]]>([ + [ + "2591orai, 773ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78", + ["2591", "orai", "773", "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"] + ], + ["foo", []], + ["2591orai", []] + ])("test-parseWithdrawLiquidityAssets", (assets, expectedParsing) => { + // act + const result = helper.parseWithdrawLiquidityAssets(assets); + // assert + expect(result).toEqual(expectedParsing); + }); + + it.each<[number, number]>([ + [1, 0], + [0, 1] + ])("test-parseTxToMsgExecuteContractMsgs-with-different-tx-code", (txCode, expectedMsgLength) => { + // setup + const cosmosTx = CosmosTx.encode( + CosmosTx.fromPartial({ body: { messages: [{ typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract" }] } }) + ).finish(); + let tx: Tx = { + hash: "", + height: 1, + code: txCode, + txIndex: 0, + tx: cosmosTx, + timestamp: new Date().toISOString(), + rawLog: JSON.stringify({ events: [] }), + events: [], + msgResponses: [{ typeUrl: "", value: Buffer.from("") }], + gasUsed: 1, + gasWanted: 1 + }; + const msgs = parseTxToMsgExecuteContractMsgs(tx); + expect(msgs.length).toEqual(expectedMsgLength); + }); + + it.each<[Uint8Array, number]>([ + [CosmosTx.encode(CosmosTx.fromPartial({})).finish(), 0], + [ + CosmosTx.encode( + CosmosTx.fromPartial({ body: { messages: [{ typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract" }] } }) + ).finish(), + 1 + ] + ])("test-parseTxToMsgExecuteContractMsgs-with-different-txBody-data", (cosmosTx, expectedMsgLength) => { + // setup + let tx: Tx = { + hash: "", + height: 1, + code: 0, + txIndex: 0, + tx: cosmosTx, + timestamp: new Date().toISOString(), + rawLog: JSON.stringify({ events: [] }), + events: [], + msgResponses: [{ typeUrl: "", value: Buffer.from("") }], + gasUsed: 1, + gasWanted: 1 + }; + const msgs = parseTxToMsgExecuteContractMsgs(tx); + expect(msgs.length).toEqual(expectedMsgLength); + }); + + it.each<[string, number]>([ + ["", 1], + ["/cosmwasm.wasm.v1.MsgExecuteContract", 2] + ])("test-parseTxToMsgExecuteContractMsgs-with-different-message-typeUrl", (typeUrl, expectedMsgLength) => { + // setup + const cosmosTx = CosmosTx.encode( + CosmosTx.fromPartial({ body: { messages: [{ typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract" }, { typeUrl }] } }) + ).finish(); + let tx: Tx = { + hash: "", + height: 1, + code: 0, + txIndex: 0, + tx: cosmosTx, + timestamp: new Date().toISOString(), + rawLog: JSON.stringify({ events: [] }), + events: [], + msgResponses: [{ typeUrl: "", value: Buffer.from("") }], + gasUsed: 1, + gasWanted: 1 + }; + const msgs = parseTxToMsgExecuteContractMsgs(tx); + expect(msgs.length).toEqual(expectedMsgLength); + }); +}); From e676244007a753cee4ee0f4f4842e6ce61c8247a Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 13 Jul 2023 17:11:07 +0700 Subject: [PATCH 13/75] added timestamp in db --- packages/oraidex-sync/src/db.ts | 4 +-- packages/oraidex-sync/src/index.ts | 4 +-- packages/oraidex-sync/src/tx-parsing.ts | 37 +++++++++++++------------ packages/oraidex-sync/src/types.ts | 9 ++++-- packages/oraidex-sync/tests/db.spec.ts | 2 ++ 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 7cdf37b3..d2f78fc4 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -47,7 +47,7 @@ export class DuckDb { async createSwapOpsTable() { const db = await this.initDuckDbConnection(); await db.exec( - "CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, offerDenom VARCHAR, offerAmount UBIGINT, askDenom VARCHAR, returnAmount UBIGINT, taxAmount UBIGINT, commissionAmount UBIGINT, spreadAmount UBIGINT)" + "CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, timestamp TIMESTAMP, offerDenom VARCHAR, offerAmount UBIGINT, askDenom VARCHAR, returnAmount UBIGINT, taxAmount UBIGINT, commissionAmount UBIGINT, spreadAmount UBIGINT)" ); } @@ -64,7 +64,7 @@ export class DuckDb { await db.exec("CREATE TYPE LPOPTYPE AS ENUM ('provide','withdraw');"); } await db.exec( - "CREATE TABLE IF NOT EXISTS lp_ops_data (txhash VARCHAR, firstTokenAmount UBIGINT, firstTokenDenom VARCHAR, secondTokenAmount UBIGINT, secondTokenDenom VARCHAR, txCreator VARCHAR, opType LPOPTYPE)" + "CREATE TABLE IF NOT EXISTS lp_ops_data (txhash VARCHAR, timestamp TIMESTAMP, firstTokenAmount UBIGINT, firstTokenDenom VARCHAR, secondTokenAmount UBIGINT, secondTokenDenom VARCHAR, txCreator VARCHAR, opType LPOPTYPE)" ); } diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 94e11ee7..7cd2a417 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -48,8 +48,8 @@ const sync = async () => { offset: currentInd, rpcUrl, queryTags: [], - limit: 100, - maxThreadLevel: 1, + limit: 1000, + maxThreadLevel: 3, interval: 5000 }).pipe(new WriteOrders()); } catch (error) { diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index fa581874..278c8340 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -51,7 +51,7 @@ function parseTxToMsgExecuteContractMsgs(tx: Tx): MsgExecuteContractWithLogs[] { return msgs; } -function extractSwapOperations(txhash: string, events: readonly Event[]): SwapOperationData[] { +function extractSwapOperations(events: readonly Event[]): SwapOperationData[] { const wasmAttributes = parseWasmEvents(events); let swapData: SwapOperationData[] = []; let offerDenoms: string[] = []; @@ -90,7 +90,8 @@ function extractSwapOperations(txhash: string, events: readonly Event[]): SwapOp // TODO: check length of above data should be equal because otherwise we would miss information for (let i = 0; i < askDenoms.length; i++) { swapData.push({ - txhash, + txhash: "", + timestamp: "", offerDenom: offerDenoms[i], offerAmount: offerAmounts[i], askDenom: askDenoms[i], @@ -103,16 +104,13 @@ function extractSwapOperations(txhash: string, events: readonly Event[]): SwapOp return swapData; } -function extractMsgProvideLiquidity( - txhash: string, - msg: MsgType, - txCreator: string -): ProvideLiquidityOperationData | undefined { +function extractMsgProvideLiquidity(msg: MsgType, txCreator: string): ProvideLiquidityOperationData | undefined { if ("provide_liquidity" in msg) { const firstAsset = msg.provide_liquidity.assets[0]; const secAsset = msg.provide_liquidity.assets[1]; return { - txhash, + txhash: "", + timestamp: "", firstTokenAmount: parseInt(firstAsset.amount), firstTokenDenom: parseAssetInfo(firstAsset.info), secondTokenAmount: parseInt(secAsset.amount), @@ -132,11 +130,7 @@ function parseWithdrawLiquidityAssets(assets: string): string[] { return matches.slice(1, 5); } -function extractMsgWithdrawLiquidity( - txhash: string, - events: readonly Event[], - txCreator: string -): WithdrawLiquidityOperationData[] { +function extractMsgWithdrawLiquidity(events: readonly Event[], txCreator: string): WithdrawLiquidityOperationData[] { const withdrawData: WithdrawLiquidityOperationData[] = []; const wasmAttributes = parseWasmEvents(events); @@ -148,7 +142,8 @@ function extractMsgWithdrawLiquidity( // sanity check. only push data if can parse asset successfully if (assets.length !== 4) continue; withdrawData.push({ - txhash, + txhash: "", + timestamp: "", firstTokenAmount: parseInt(assets[0]), firstTokenDenom: assets[1], secondTokenAmount: parseInt(assets[2]), @@ -204,10 +199,18 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { const txhash = tx.hash; for (let msg of msgs) { const sender = msg.sender; - swapOpsData.push(...extractSwapOperations(txhash, msg.logs.events)); - const provideLiquidityData = extractMsgProvideLiquidity(txhash, msg.msg, sender); + swapOpsData.push( + ...extractSwapOperations(msg.logs.events).map((op) => ({ txhash, timestamp: tx.timestamp, ...op })) + ); + const provideLiquidityData = { txhash, timestamp: tx.timestamp, ...extractMsgProvideLiquidity(msg.msg, sender) }; if (provideLiquidityData) provideLiquidityOpsData.push(provideLiquidityData); - withdrawLiquidityOpsData.push(...extractMsgWithdrawLiquidity(txhash, msg.logs.events, sender)); + withdrawLiquidityOpsData.push( + ...extractMsgWithdrawLiquidity(msg.logs.events, sender).map((op) => ({ + txhash, + timestamp: tx.timestamp, + ...op + })) + ); accountTxs.push({ txhash, accountAddress: sender }); } } diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 1b0194d1..a20734c5 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -11,8 +11,12 @@ export type AssetData = { name: string; }; -export type SwapOperationData = { +export type BasicTxData = { txhash: string; + timestamp: string; +}; + +export type SwapOperationData = BasicTxData & { offerDenom: string; offerAmount: string; askDenom: string; @@ -29,8 +33,7 @@ export type AccountTx = { export type LiquidityOpType = "provide" | "withdraw"; -export type ProvideLiquidityOperationData = { - txhash: string; +export type ProvideLiquidityOperationData = BasicTxData & { firstTokenAmount: number; firstTokenDenom: string; secondTokenAmount: number; diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index 509afdd3..69aaf238 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -15,6 +15,7 @@ describe("test-duckdb", () => { duckDb.insertLpOps([ { txhash: "foo", + timestamp: new Date().toISOString(), firstTokenAmount: "abcd" as any, firstTokenDenom: "orai", secondTokenAmount: 2, @@ -31,6 +32,7 @@ describe("test-duckdb", () => { await duckDb.insertLpOps([ { txhash: "foo", + timestamp: new Date().toISOString(), firstTokenAmount: 1, firstTokenDenom: "orai", secondTokenAmount: 2, From e9b7a5b07e96266b7397bf143ec69b9d6840a73d Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 13 Jul 2023 18:25:25 +0700 Subject: [PATCH 14/75] added tx timestamp when syncing --- package.json | 10 ++--- packages/oraidex-sync/src/index.ts | 2 +- packages/oraidex-sync/src/tx-parsing.ts | 38 ++++++++++--------- packages/oraidex-sync/tests/db.spec.ts | 4 +- patches/@cosmjs+stargate+0.31.0.patch | 12 ++++++ ...ch => @cosmjs+tendermint-rpc+0.31.0.patch} | 22 ++++++++--- yarn.lock | 17 +++++++++ 7 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 patches/@cosmjs+stargate+0.31.0.patch rename patches/{@cosmjs+tendermint-rpc+0.29.5.patch => @cosmjs+tendermint-rpc+0.31.0.patch} (50%) diff --git a/package.json b/package.json index a955be0e..e61005af 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,11 @@ "node": ">=16 <=18" }, "dependencies": { - "@cosmjs/amino": "^0.29.5", - "@cosmjs/cosmwasm-stargate": "^0.29.5", - "@cosmjs/crypto": "^0.29.5", - "@cosmjs/proto-signing": "^0.29.5", - "@cosmjs/stargate": "^0.29.5" + "@cosmjs/amino": "^0.31.0", + "@cosmjs/cosmwasm-stargate": "^0.31.0", + "@cosmjs/crypto": "^0.31.0", + "@cosmjs/proto-signing": "^0.31.0", + "@cosmjs/stargate": "^0.31.0" }, "devDependencies": { "@terran-one/cw-simulate": "https://github.com/oraichain/cw-simulate.git", diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 7cd2a417..e1134ef0 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -48,7 +48,7 @@ const sync = async () => { offset: currentInd, rpcUrl, queryTags: [], - limit: 1000, + limit: 100, maxThreadLevel: 3, interval: 5000 }).pipe(new WriteOrders()); diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 278c8340..89e7bf84 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -51,7 +51,7 @@ function parseTxToMsgExecuteContractMsgs(tx: Tx): MsgExecuteContractWithLogs[] { return msgs; } -function extractSwapOperations(events: readonly Event[]): SwapOperationData[] { +function extractSwapOperations(txhash: string, timestamp: string, events: readonly Event[]): SwapOperationData[] { const wasmAttributes = parseWasmEvents(events); let swapData: SwapOperationData[] = []; let offerDenoms: string[] = []; @@ -90,8 +90,8 @@ function extractSwapOperations(events: readonly Event[]): SwapOperationData[] { // TODO: check length of above data should be equal because otherwise we would miss information for (let i = 0; i < askDenoms.length; i++) { swapData.push({ - txhash: "", - timestamp: "", + txhash, + timestamp, offerDenom: offerDenoms[i], offerAmount: offerAmounts[i], askDenom: askDenoms[i], @@ -104,13 +104,18 @@ function extractSwapOperations(events: readonly Event[]): SwapOperationData[] { return swapData; } -function extractMsgProvideLiquidity(msg: MsgType, txCreator: string): ProvideLiquidityOperationData | undefined { +function extractMsgProvideLiquidity( + txhash: string, + timestamp: string, + msg: MsgType, + txCreator: string +): ProvideLiquidityOperationData | undefined { if ("provide_liquidity" in msg) { const firstAsset = msg.provide_liquidity.assets[0]; const secAsset = msg.provide_liquidity.assets[1]; return { - txhash: "", - timestamp: "", + txhash, + timestamp, firstTokenAmount: parseInt(firstAsset.amount), firstTokenDenom: parseAssetInfo(firstAsset.info), secondTokenAmount: parseInt(secAsset.amount), @@ -130,7 +135,12 @@ function parseWithdrawLiquidityAssets(assets: string): string[] { return matches.slice(1, 5); } -function extractMsgWithdrawLiquidity(events: readonly Event[], txCreator: string): WithdrawLiquidityOperationData[] { +function extractMsgWithdrawLiquidity( + txhash: string, + timestamp: string, + events: readonly Event[], + txCreator: string +): WithdrawLiquidityOperationData[] { const withdrawData: WithdrawLiquidityOperationData[] = []; const wasmAttributes = parseWasmEvents(events); @@ -199,18 +209,10 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { const txhash = tx.hash; for (let msg of msgs) { const sender = msg.sender; - swapOpsData.push( - ...extractSwapOperations(msg.logs.events).map((op) => ({ txhash, timestamp: tx.timestamp, ...op })) - ); - const provideLiquidityData = { txhash, timestamp: tx.timestamp, ...extractMsgProvideLiquidity(msg.msg, sender) }; + swapOpsData.push(...extractSwapOperations(txhash, tx.timestamp, msg.logs.events)); + const provideLiquidityData = extractMsgProvideLiquidity(txhash, tx.timestamp, msg.msg, sender); if (provideLiquidityData) provideLiquidityOpsData.push(provideLiquidityData); - withdrawLiquidityOpsData.push( - ...extractMsgWithdrawLiquidity(msg.logs.events, sender).map((op) => ({ - txhash, - timestamp: tx.timestamp, - ...op - })) - ); + withdrawLiquidityOpsData.push(...extractMsgWithdrawLiquidity(txhash, tx.timestamp, msg.logs.events, sender)); accountTxs.push({ txhash, accountAddress: sender }); } } diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index 69aaf238..92704742 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -2,10 +2,10 @@ import { DuckDb } from "../src/db"; describe("test-duckdb", () => { let duckDb: DuckDb; - beforeEach(async () => { + beforeAll(async () => { // fixture duckDb = new DuckDb(); - await duckDb.initDuckDb(); + await duckDb.initDuckDb("oraidex-sync-data-test"); await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); }); diff --git a/patches/@cosmjs+stargate+0.31.0.patch b/patches/@cosmjs+stargate+0.31.0.patch new file mode 100644 index 00000000..35cecf68 --- /dev/null +++ b/patches/@cosmjs+stargate+0.31.0.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/@cosmjs/stargate/build/stargateclient.js b/node_modules/@cosmjs/stargate/build/stargateclient.js +index 3268b13..70bbbec 100644 +--- a/node_modules/@cosmjs/stargate/build/stargateclient.js ++++ b/node_modules/@cosmjs/stargate/build/stargateclient.js +@@ -299,6 +299,7 @@ class StargateClient { + return results.txs.map((tx) => { + const txMsgData = abci_1.TxMsgData.decode(tx.result.data ?? new Uint8Array()); + return { ++ ...tx, + height: tx.height, + txIndex: tx.index, + hash: (0, encoding_1.toHex)(tx.hash).toUpperCase(), diff --git a/patches/@cosmjs+tendermint-rpc+0.29.5.patch b/patches/@cosmjs+tendermint-rpc+0.31.0.patch similarity index 50% rename from patches/@cosmjs+tendermint-rpc+0.29.5.patch rename to patches/@cosmjs+tendermint-rpc+0.31.0.patch index 5a434cfa..dd45c8b5 100644 --- a/patches/@cosmjs+tendermint-rpc+0.29.5.patch +++ b/patches/@cosmjs+tendermint-rpc+0.31.0.patch @@ -1,25 +1,37 @@ diff --git a/node_modules/@cosmjs/tendermint-rpc/build/rpcclients/http.js b/node_modules/@cosmjs/tendermint-rpc/build/rpcclients/http.js -index 4b83a96..b8a1102 100644 +index 8c72817..2b810d2 100644 --- a/node_modules/@cosmjs/tendermint-rpc/build/rpcclients/http.js +++ b/node_modules/@cosmjs/tendermint-rpc/build/rpcclients/http.js -@@ -18,9 +18,11 @@ function filterBadStatus(res) { +@@ -34,9 +34,11 @@ function isExperimental(nodeJsFunc) { */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async function http(method, url, headers, request) { + const timeout = Number(process.env.HTTP_TIMEOUT || 30000); - if (typeof fetch !== "undefined") { + if (typeof fetch === "function" && !isExperimental(fetch)) { const settings = { method: method, + signal: AbortSignal.timeout(timeout), body: request ? JSON.stringify(request) : undefined, headers: { // eslint-disable-next-line @typescript-eslint/naming-convention -@@ -34,7 +36,7 @@ async function http(method, url, headers, request) { +@@ -50,7 +52,7 @@ async function http(method, url, headers, request) { } else { return axios_1.default - .request({ url: url, method: method, data: request, headers: headers }) -+ .request({ url: url, method: method, data: request, headers: headers, timeout: timeout}) ++ .request({ url: url, method: method, data: request, headers: headers, timeout: timeout }) .then((res) => res.data); } } +diff --git a/node_modules/@cosmjs/tendermint-rpc/build/tendermint34/adaptor/responses.js b/node_modules/@cosmjs/tendermint-rpc/build/tendermint34/adaptor/responses.js +index b9e9e57..1f11a22 100644 +--- a/node_modules/@cosmjs/tendermint-rpc/build/tendermint34/adaptor/responses.js ++++ b/node_modules/@cosmjs/tendermint-rpc/build/tendermint34/adaptor/responses.js +@@ -310,6 +310,7 @@ function decodeTxProof(data) { + } + function decodeTxResponse(data) { + return { ++ ...data, + tx: (0, encoding_1.fromBase64)((0, encodings_1.assertNotEmpty)(data.tx)), + result: decodeTxData((0, encodings_1.assertObject)(data.tx_result)), + height: (0, inthelpers_1.apiToSmallInt)((0, encodings_1.assertNotEmpty)(data.height)), diff --git a/yarn.lock b/yarn.lock index 77099aee..c74a5613 100644 --- a/yarn.lock +++ b/yarn.lock @@ -365,6 +365,23 @@ long "^4.0.0" pako "^2.0.2" +"@cosmjs/cosmwasm-stargate@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.31.0.tgz#a9ea82471ca035b8d7f6ae640ad44b5f497be8c6" + integrity sha512-l6aX++3LhaAGZO46qIgrrNF40lYhOrdPfl35Z32ks6Wf3mwgbQEZwaxnoGzwUePY7/yaIiEFJ1JO6MlVPZVuag== + dependencies: + "@cosmjs/amino" "^0.31.0" + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/proto-signing" "^0.31.0" + "@cosmjs/stargate" "^0.31.0" + "@cosmjs/tendermint-rpc" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + cosmjs-types "^0.8.0" + long "^4.0.0" + pako "^2.0.2" + "@cosmjs/crypto@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.29.5.tgz#ab99fc382b93d8a8db075780cf07487a0f9519fd" From eea1de56b1001ba7a0bdfe0fab53f2bcf6277cce Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 13 Jul 2023 18:32:18 +0700 Subject: [PATCH 15/75] fixed extractMsgWithdrawLiquidity did not use txhash and timestamp --- packages/oraidex-sync/src/tx-parsing.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 89e7bf84..8f285f75 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -152,8 +152,8 @@ function extractMsgWithdrawLiquidity( // sanity check. only push data if can parse asset successfully if (assets.length !== 4) continue; withdrawData.push({ - txhash: "", - timestamp: "", + txhash, + timestamp, firstTokenAmount: parseInt(assets[0]), firstTokenDenom: assets[1], secondTokenAmount: parseInt(assets[2]), From 3e5c20a482d92ccff1dbf25ec4f1d4fc9d937b37 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 14 Jul 2023 11:08:40 +0700 Subject: [PATCH 16/75] patched cosmwasm now can query past data given height --- .../@cosmjs+cosmwasm-stargate+0.31.0.patch | 73 +++++++++++++++++++ patches/@cosmjs+stargate+0.31.0.patch | 48 ++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 patches/@cosmjs+cosmwasm-stargate+0.31.0.patch diff --git a/patches/@cosmjs+cosmwasm-stargate+0.31.0.patch b/patches/@cosmjs+cosmwasm-stargate+0.31.0.patch new file mode 100644 index 00000000..e91fa0a0 --- /dev/null +++ b/patches/@cosmjs+cosmwasm-stargate+0.31.0.patch @@ -0,0 +1,73 @@ +diff --git a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts +index 3aebdc5..71387b2 100644 +--- a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts ++++ b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts +@@ -49,12 +49,12 @@ export declare class CosmWasmClient { + * This uses auto-detection to decide between a Tendermint 0.37 and 0.34 client. + * To set the Tendermint client explicitly, use `create`. + */ +- static connect(endpoint: string | HttpEndpoint): Promise; ++ static connect(endpoint: string | HttpEndpoint, height?: number): Promise; + /** + * Creates an instance from a manually created Tendermint client. + * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. + */ +- static create(tmClient: TendermintClient): Promise; ++ static create(tmClient: TendermintClient, height?: number): Promise; + protected constructor(tmClient: TendermintClient | undefined); + protected getTmClient(): TendermintClient | undefined; + protected forceGetTmClient(): TendermintClient; +diff --git a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js +index 8f6305b..ae1944b 100644 +--- a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js ++++ b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js +@@ -17,7 +17,7 @@ class CosmWasmClient { + * This uses auto-detection to decide between a Tendermint 0.37 and 0.34 client. + * To set the Tendermint client explicitly, use `create`. + */ +- static async connect(endpoint) { ++ static async connect(endpoint, height = undefined) { + // Tendermint/CometBFT 0.34/0.37 auto-detection. Starting with 0.37 we seem to get reliable versions again 🎉 + // Using 0.34 as the fallback. + let tmClient; +@@ -30,20 +30,20 @@ class CosmWasmClient { + tm37Client.disconnect(); + tmClient = await tendermint_rpc_1.Tendermint34Client.connect(endpoint); + } +- return CosmWasmClient.create(tmClient); ++ return CosmWasmClient.create(tmClient, height); + } + /** + * Creates an instance from a manually created Tendermint client. + * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. + */ +- static async create(tmClient) { +- return new CosmWasmClient(tmClient); ++ static async create(tmClient, height) { ++ return new CosmWasmClient(tmClient, height); + } +- constructor(tmClient) { ++ constructor(tmClient, height = undefined) { + this.codesCache = new Map(); + if (tmClient) { + this.tmClient = tmClient; +- this.queryClient = stargate_1.QueryClient.withExtensions(tmClient, stargate_1.setupAuthExtension, stargate_1.setupBankExtension, modules_1.setupWasmExtension, stargate_1.setupTxExtension); ++ this.queryClient = stargate_1.QueryClient.withExtensions(tmClient, height, stargate_1.setupAuthExtension, stargate_1.setupBankExtension, modules_1.setupWasmExtension, stargate_1.setupTxExtension); + } + } + getTmClient() { +diff --git a/node_modules/@cosmjs/cosmwasm-stargate/build/modules/wasm/queries.js b/node_modules/@cosmjs/cosmwasm-stargate/build/modules/wasm/queries.js +index c060369..41029c3 100644 +--- a/node_modules/@cosmjs/cosmwasm-stargate/build/modules/wasm/queries.js ++++ b/node_modules/@cosmjs/cosmwasm-stargate/build/modules/wasm/queries.js +@@ -8,8 +8,8 @@ const encoding_1 = require("@cosmjs/encoding"); + const stargate_1 = require("@cosmjs/stargate"); + const query_1 = require("cosmjs-types/cosmwasm/wasm/v1/query"); + const long_1 = __importDefault(require("long")); +-function setupWasmExtension(base) { +- const rpc = (0, stargate_1.createProtobufRpcClient)(base); ++function setupWasmExtension(base, height) { ++ const rpc = (0, stargate_1.createProtobufRpcClient)(base, height); + // Use this service to get easy typed access to query methods + // This cannot be used for proof verification + const queryService = new query_1.QueryClientImpl(rpc); diff --git a/patches/@cosmjs+stargate+0.31.0.patch b/patches/@cosmjs+stargate+0.31.0.patch index 35cecf68..57fa0d67 100644 --- a/patches/@cosmjs+stargate+0.31.0.patch +++ b/patches/@cosmjs+stargate+0.31.0.patch @@ -1,3 +1,51 @@ +diff --git a/node_modules/@cosmjs/stargate/build/queryclient/queryclient.js b/node_modules/@cosmjs/stargate/build/queryclient/queryclient.js +index ebf0c7c..7a0ddce 100644 +--- a/node_modules/@cosmjs/stargate/build/queryclient/queryclient.js ++++ b/node_modules/@cosmjs/stargate/build/queryclient/queryclient.js +@@ -16,9 +16,9 @@ function checkAndParseOp(op, kind, key) { + return ics23_1.ics23.CommitmentProof.decode(op.data); + } + class QueryClient { +- static withExtensions(tmClient, ...extensionSetups) { +- const client = new QueryClient(tmClient); +- const extensions = extensionSetups.map((setupExtension) => setupExtension(client)); ++ static withExtensions(tmClient, height = undefined, ...extensionSetups) { ++ const client = new QueryClient(tmClient, height); ++ const extensions = extensionSetups.map((setupExtension) => setupExtension(client, height)); + for (const extension of extensions) { + (0, utils_1.assert)((0, utils_1.isNonNullObject)(extension), `Extension must be a non-null object`); + for (const [moduleKey, moduleValue] of Object.entries(extension)) { +diff --git a/node_modules/@cosmjs/stargate/build/queryclient/utils.d.ts b/node_modules/@cosmjs/stargate/build/queryclient/utils.d.ts +index 5317078..75d149e 100644 +--- a/node_modules/@cosmjs/stargate/build/queryclient/utils.d.ts ++++ b/node_modules/@cosmjs/stargate/build/queryclient/utils.d.ts +@@ -19,7 +19,7 @@ export declare function createPagination(paginationKey?: Uint8Array): PageReques + export interface ProtobufRpcClient { + request(service: string, method: string, data: Uint8Array): Promise; + } +-export declare function createProtobufRpcClient(base: QueryClient): ProtobufRpcClient; ++export declare function createProtobufRpcClient(base: QueryClient, height?: number): ProtobufRpcClient; + /** + * Takes a uint64 value as string, number, Long or Uint64 and returns an unsigned Long instance + * of it. +diff --git a/node_modules/@cosmjs/stargate/build/queryclient/utils.js b/node_modules/@cosmjs/stargate/build/queryclient/utils.js +index 6f4ecaa..7816bad 100644 +--- a/node_modules/@cosmjs/stargate/build/queryclient/utils.js ++++ b/node_modules/@cosmjs/stargate/build/queryclient/utils.js +@@ -28,11 +28,11 @@ function createPagination(paginationKey) { + return paginationKey ? pagination_1.PageRequest.fromPartial({ key: paginationKey }) : undefined; + } + exports.createPagination = createPagination; +-function createProtobufRpcClient(base) { ++function createProtobufRpcClient(base, height = undefined) { + return { + request: async (service, method, data) => { + const path = `/${service}/${method}`; +- const response = await base.queryAbci(path, data, undefined); ++ const response = await base.queryAbci(path, data, height); + return response.value; + }, + }; diff --git a/node_modules/@cosmjs/stargate/build/stargateclient.js b/node_modules/@cosmjs/stargate/build/stargateclient.js index 3268b13..70bbbec 100644 --- a/node_modules/@cosmjs/stargate/build/stargateclient.js From 6473e5641b6db019e46266dfec74c754cf08aae6 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 14 Jul 2023 12:18:46 +0700 Subject: [PATCH 17/75] updated patch cosmwasm & stargate to make query by height simpler --- .../@cosmjs+cosmwasm-stargate+0.31.0.patch | 62 +++++-------------- patches/@cosmjs+stargate+0.31.0.patch | 36 +++++++---- 2 files changed, 41 insertions(+), 57 deletions(-) diff --git a/patches/@cosmjs+cosmwasm-stargate+0.31.0.patch b/patches/@cosmjs+cosmwasm-stargate+0.31.0.patch index e91fa0a0..a93229a4 100644 --- a/patches/@cosmjs+cosmwasm-stargate+0.31.0.patch +++ b/patches/@cosmjs+cosmwasm-stargate+0.31.0.patch @@ -1,61 +1,33 @@ diff --git a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts -index 3aebdc5..71387b2 100644 +index 3aebdc5..d5cbeec 100644 --- a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts +++ b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts -@@ -49,12 +49,12 @@ export declare class CosmWasmClient { - * This uses auto-detection to decide between a Tendermint 0.37 and 0.34 client. - * To set the Tendermint client explicitly, use `create`. +@@ -56,6 +56,7 @@ export declare class CosmWasmClient { */ -- static connect(endpoint: string | HttpEndpoint): Promise; -+ static connect(endpoint: string | HttpEndpoint, height?: number): Promise; - /** - * Creates an instance from a manually created Tendermint client. - * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. - */ -- static create(tmClient: TendermintClient): Promise; -+ static create(tmClient: TendermintClient, height?: number): Promise; + static create(tmClient: TendermintClient): Promise; protected constructor(tmClient: TendermintClient | undefined); ++ public setQueryClientWithHeight(height?: number): void; protected getTmClient(): TendermintClient | undefined; protected forceGetTmClient(): TendermintClient; + protected getQueryClient(): (QueryClient & AuthExtension & BankExtension & TxExtension & WasmExtension) | undefined; diff --git a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js -index 8f6305b..ae1944b 100644 +index 8f6305b..90cedba 100644 --- a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js +++ b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js -@@ -17,7 +17,7 @@ class CosmWasmClient { - * This uses auto-detection to decide between a Tendermint 0.37 and 0.34 client. - * To set the Tendermint client explicitly, use `create`. - */ -- static async connect(endpoint) { -+ static async connect(endpoint, height = undefined) { - // Tendermint/CometBFT 0.34/0.37 auto-detection. Starting with 0.37 we seem to get reliable versions again 🎉 - // Using 0.34 as the fallback. - let tmClient; -@@ -30,20 +30,20 @@ class CosmWasmClient { - tm37Client.disconnect(); - tmClient = await tendermint_rpc_1.Tendermint34Client.connect(endpoint); - } -- return CosmWasmClient.create(tmClient); -+ return CosmWasmClient.create(tmClient, height); - } - /** - * Creates an instance from a manually created Tendermint client. - * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. - */ -- static async create(tmClient) { -- return new CosmWasmClient(tmClient); -+ static async create(tmClient, height) { -+ return new CosmWasmClient(tmClient, height); - } -- constructor(tmClient) { -+ constructor(tmClient, height = undefined) { - this.codesCache = new Map(); - if (tmClient) { - this.tmClient = tmClient; -- this.queryClient = stargate_1.QueryClient.withExtensions(tmClient, stargate_1.setupAuthExtension, stargate_1.setupBankExtension, modules_1.setupWasmExtension, stargate_1.setupTxExtension); -+ this.queryClient = stargate_1.QueryClient.withExtensions(tmClient, height, stargate_1.setupAuthExtension, stargate_1.setupBankExtension, modules_1.setupWasmExtension, stargate_1.setupTxExtension); +@@ -46,6 +46,13 @@ class CosmWasmClient { + this.queryClient = stargate_1.QueryClient.withExtensions(tmClient, stargate_1.setupAuthExtension, stargate_1.setupBankExtension, modules_1.setupWasmExtension, stargate_1.setupTxExtension); } } ++ ++ setQueryClientWithHeight(height = undefined) { ++ if (this.tmClient) { ++ this.queryClient = stargate_1.QueryClient.withExtensionsWithHeight(this.tmClient, height, stargate_1.setupAuthExtension, stargate_1.setupBankExtension, modules_1.setupWasmExtension, stargate_1.setupTxExtension); ++ } ++ } ++ getTmClient() { + return this.tmClient; + } diff --git a/node_modules/@cosmjs/cosmwasm-stargate/build/modules/wasm/queries.js b/node_modules/@cosmjs/cosmwasm-stargate/build/modules/wasm/queries.js index c060369..41029c3 100644 --- a/node_modules/@cosmjs/cosmwasm-stargate/build/modules/wasm/queries.js diff --git a/patches/@cosmjs+stargate+0.31.0.patch b/patches/@cosmjs+stargate+0.31.0.patch index 57fa0d67..52cb7812 100644 --- a/patches/@cosmjs+stargate+0.31.0.patch +++ b/patches/@cosmjs+stargate+0.31.0.patch @@ -1,20 +1,32 @@ diff --git a/node_modules/@cosmjs/stargate/build/queryclient/queryclient.js b/node_modules/@cosmjs/stargate/build/queryclient/queryclient.js -index ebf0c7c..7a0ddce 100644 +index ebf0c7c..afa645f 100644 --- a/node_modules/@cosmjs/stargate/build/queryclient/queryclient.js +++ b/node_modules/@cosmjs/stargate/build/queryclient/queryclient.js -@@ -16,9 +16,9 @@ function checkAndParseOp(op, kind, key) { - return ics23_1.ics23.CommitmentProof.decode(op.data); - } - class QueryClient { -- static withExtensions(tmClient, ...extensionSetups) { -- const client = new QueryClient(tmClient); -- const extensions = extensionSetups.map((setupExtension) => setupExtension(client)); -+ static withExtensions(tmClient, height = undefined, ...extensionSetups) { +@@ -32,6 +32,24 @@ class QueryClient { + } + return client; + } ++ ++ static withExtensionsWithHeight(tmClient, height, ...extensionSetups) { + const client = new QueryClient(tmClient, height); + const extensions = extensionSetups.map((setupExtension) => setupExtension(client, height)); - for (const extension of extensions) { - (0, utils_1.assert)((0, utils_1.isNonNullObject)(extension), `Extension must be a non-null object`); - for (const [moduleKey, moduleValue] of Object.entries(extension)) { ++ for (const extension of extensions) { ++ (0, utils_1.assert)((0, utils_1.isNonNullObject)(extension), `Extension must be a non-null object`); ++ for (const [moduleKey, moduleValue] of Object.entries(extension)) { ++ (0, utils_1.assert)((0, utils_1.isNonNullObject)(moduleValue), `Module must be a non-null object. Found type ${typeof moduleValue} for module "${moduleKey}".`); ++ const current = client[moduleKey] || {}; ++ client[moduleKey] = { ++ ...current, ++ ...moduleValue, ++ }; ++ } ++ } ++ return client; ++ } ++ + constructor(tmClient) { + this.tmClient = tmClient; + } diff --git a/node_modules/@cosmjs/stargate/build/queryclient/utils.d.ts b/node_modules/@cosmjs/stargate/build/queryclient/utils.d.ts index 5317078..75d149e 100644 --- a/node_modules/@cosmjs/stargate/build/queryclient/utils.d.ts From c9d9d5bfd22651a652e97fcad67836cb9f2d1056 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 14 Jul 2023 15:02:25 +0700 Subject: [PATCH 18/75] refactored duckdb code & added poc prefix sum --- packages/oraidex-sync/.env.example | 4 +- packages/oraidex-sync/src/db.ts | 44 +++------ packages/oraidex-sync/src/index.ts | 124 +++++++++++++++++++------ packages/oraidex-sync/src/poc.ts | 22 +++++ packages/oraidex-sync/src/tx-insert.ts | 26 ------ packages/oraidex-sync/src/tx-query.ts | 11 --- 6 files changed, 135 insertions(+), 96 deletions(-) create mode 100644 packages/oraidex-sync/src/poc.ts delete mode 100644 packages/oraidex-sync/src/tx-insert.ts delete mode 100644 packages/oraidex-sync/src/tx-query.ts diff --git a/packages/oraidex-sync/.env.example b/packages/oraidex-sync/.env.example index 118262d7..5ab5ed1e 100644 --- a/packages/oraidex-sync/.env.example +++ b/packages/oraidex-sync/.env.example @@ -1 +1,3 @@ -RPC_URL=https://rpc.orai.io/ \ No newline at end of file +RPC_URL=https://rpc.orai.io/ +FACTORY_CONTACT_ADDRESS_V1="orai1hemdkz4xx9kukgrunxu3yw0nvpyxf34v82d2c8" +FACTORY_CONTACT_ADDRESS_V2="orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" \ No newline at end of file diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index d2f78fc4..f80ee19d 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -3,50 +3,39 @@ import { SwapOperationData, WithdrawLiquidityOperationData } from "./types"; import fs from "fs"; export class DuckDb { - private db: Database; + protected constructor(public readonly conn: Connection) {} - closeDuckDb(): void { - this.db.close(); - } - - async initDuckDb(fileName?: string): Promise { - this.db = await Database.create(fileName ?? "oraidex-sync-data"); - } - - async initDuckDbConnection(): Promise { - return this.db.connect(); + static async create(fileName?: string): Promise { + const db = await Database.create(fileName ?? "data"); + const conn = await db.connect(); + return new DuckDb(conn); } async createHeightSnapshot() { - const db = await this.initDuckDbConnection(); - await db.exec("CREATE TABLE IF NOT EXISTS height_snapshot (currentInd UINTEGER,PRIMARY KEY (currentInd))"); + await this.conn.exec("CREATE TABLE IF NOT EXISTS height_snapshot (currentInd UINTEGER,PRIMARY KEY (currentInd))"); } async loadHeightSnapshot() { - const db = await this.initDuckDbConnection(); - const result = await db.all("SELECT * FROM height_snapshot"); + const result = await this.conn.all("SELECT * FROM height_snapshot"); return result.length > 0 ? result[0] : { currentInd: 1 }; } async insertHeightSnapshot(currentInd: number) { - const db = await this.initDuckDbConnection(); - await db.run("INSERT OR REPLACE INTO height_snapshot VALUES (?)", currentInd); + await this.conn.run("INSERT OR REPLACE INTO height_snapshot VALUES (?)", currentInd); } private async insertBulkData(data: any[], tableName: string) { // we wont insert anything if the data is empty. Otherwise it would throw an error while inserting if (data.length === 0) return; - const db = await this.initDuckDbConnection(); const tableFile = `${tableName}.json`; // the file written out is temporary only. Will be deleted after insertion await fs.promises.writeFile(tableFile, JSON.stringify(data)); - await db.run(`INSERT INTO ${tableName} SELECT * FROM read_json_auto(?)`, tableFile); + await this.conn.run(`INSERT INTO ${tableName} SELECT * FROM read_json_auto(?)`, tableFile); await fs.promises.unlink(tableFile); } async createSwapOpsTable() { - const db = await this.initDuckDbConnection(); - await db.exec( + await this.conn.exec( "CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, timestamp TIMESTAMP, offerDenom VARCHAR, offerAmount UBIGINT, askDenom VARCHAR, returnAmount UBIGINT, taxAmount UBIGINT, commissionAmount UBIGINT, spreadAmount UBIGINT)" ); } @@ -56,14 +45,13 @@ export class DuckDb { } async createLiquidityOpsTable() { - const db = await this.initDuckDbConnection(); try { - await db.all("select enum_range(NULL::LPOPTYPE);"); + await this.conn.all("select enum_range(NULL::LPOPTYPE);"); } catch (error) { // if error it means the enum does not exist => create new - await db.exec("CREATE TYPE LPOPTYPE AS ENUM ('provide','withdraw');"); + await this.conn.exec("CREATE TYPE LPOPTYPE AS ENUM ('provide','withdraw');"); } - await db.exec( + await this.conn.exec( "CREATE TABLE IF NOT EXISTS lp_ops_data (txhash VARCHAR, timestamp TIMESTAMP, firstTokenAmount UBIGINT, firstTokenDenom VARCHAR, secondTokenAmount UBIGINT, secondTokenDenom VARCHAR, txCreator VARCHAR, opType LPOPTYPE)" ); } @@ -73,12 +61,10 @@ export class DuckDb { } async querySwapOps() { - const db = await this.initDuckDbConnection(); - return db.all("SELECT count(*) from swap_ops_data"); + return this.conn.all("SELECT count(*) from swap_ops_data"); } async queryLpOps() { - const db = await this.initDuckDbConnection(); - return db.all("SELECT count(*) from lp_ops_data"); + return this.conn.all("SELECT count(*) from lp_ops_data"); } } diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index e1134ef0..9f243e79 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -3,26 +3,56 @@ import { parseTxs } from "./tx-parsing"; import { DuckDb } from "./db"; import { WriteData, SyncData, Txs } from "@oraichain/cosmos-rpc-sync"; import "dotenv/config"; -import { insertParsedTxs } from "./tx-insert"; -import { queryLpOps, querySwapOps } from "./tx-query"; +import { pairs } from "./pairs"; +import { CosmWasmClient, OraiswapFactoryQueryClient } from "@oraichain/oraidex-contracts-sdk"; +import { + ProvideLiquidityOperationData, + SwapOperationData, + TxAnlysisResult, + WithdrawLiquidityOperationData +} from "./types"; -const duckDb = new DuckDb(); class WriteOrders extends WriteData { - constructor() { + constructor(private duckDb: DuckDb) { super(); } + private async insertSwapOps(ops: SwapOperationData[]) { + await this.duckDb.insertSwapOps(ops); + } + + private async insertLiquidityOps(ops: ProvideLiquidityOperationData[] | WithdrawLiquidityOperationData[]) { + await this.duckDb.insertLpOps(ops); + } + + private async insertParsedTxs(txs: TxAnlysisResult) { + // insert swap ops + await Promise.all([ + this.insertSwapOps(txs.swapOpsData), + this.insertLiquidityOps(txs.provideLiquidityOpsData), + this.insertLiquidityOps(txs.withdrawLiquidityOpsData) + ]); + } + + private async querySwapOps() { + return this.duckDb.querySwapOps(); + } + + private async queryLpOps() { + return this.duckDb.queryLpOps(); + } + async process(chunk: any): Promise { try { const { txs, offset: newOffset, queryTags } = chunk as Txs; console.log("new offset: ", newOffset); const result = parseTxs(txs); // insert txs - await duckDb.insertHeightSnapshot(newOffset); - await insertParsedTxs(result); + await this.duckDb.insertHeightSnapshot(newOffset); + await this.insertParsedTxs(result); - const swapOps = await querySwapOps(); - const lpOps = await queryLpOps(); + const swapOps = await this.querySwapOps(); + const lpOps = await this.queryLpOps(); console.log("swap ops: ", swapOps); console.log("lp ops: ", lpOps); @@ -34,29 +64,65 @@ class WriteOrders extends WriteData { } } -const sync = async () => { - try { - await duckDb.initDuckDb(); - await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); - const rpcUrl = process.env.RPC_URL || "https://rpc.orai.io"; - let { currentInd } = await duckDb.loadHeightSnapshot(); - // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic - if (currentInd <= 12388825) { - currentInd = 12388825; +class OraiDexSync { + constructor(private duckDb: DuckDb, private rpcUrl: string) {} + + private async getAllPairInfos(wantedHeight?: number) { + const cosmwasmClient = await CosmWasmClient.connect(this.rpcUrl); + cosmwasmClient.setQueryClientWithHeight(wantedHeight); + const firstFactoryClient = new OraiswapFactoryQueryClient( + cosmwasmClient, + process.env.FACTORY_CONTACT_ADDRESS_V1 || "orai1hemdkz4xx9kukgrunxu3yw0nvpyxf34v82d2c8" + ); + const secondFactoryClient = new OraiswapFactoryQueryClient( + cosmwasmClient, + process.env.FACTORY_CONTACT_ADDRESS_V2 || "orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" + ); + const liquidityResults = ( + await Promise.allSettled([ + ...pairs.map((pair) => firstFactoryClient.pair({ assetInfos: pair.asset_infos })), + ...pairs.map((pair) => secondFactoryClient.pair({ assetInfos: pair.asset_infos })) + ]) + ).filter((res) => { + if (res.status === "fulfilled") return res.value; + }); + console.dir(liquidityResults, { depth: null }); + } + + public async sync() { + try { + await Promise.all([ + this.duckDb.createHeightSnapshot(), + this.duckDb.createLiquidityOpsTable(), + this.duckDb.createSwapOpsTable() + ]); + console.log("rpc url: ", this.rpcUrl); + let { currentInd } = await this.duckDb.loadHeightSnapshot(); + console.log("current ind: ", currentInd); + // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic + if (currentInd <= 12388825) { + currentInd = 12388825; + } + await this.getAllPairInfos(12389125); + // new SyncData({ + // offset: currentInd, + // rpcUrl, + // queryTags: [], + // limit: 100, + // maxThreadLevel: 3, + // interval: 5000 + // }).pipe(new WriteOrders()); + } catch (error) { + console.log("error in start: ", error); } - new SyncData({ - offset: currentInd, - rpcUrl, - queryTags: [], - limit: 100, - maxThreadLevel: 3, - interval: 5000 - }).pipe(new WriteOrders()); - } catch (error) { - console.log("error in start: ", error); } +} + +const start = async () => { + const duckDb = await DuckDb.create("oraidex-sync-data"); + new OraiDexSync(duckDb, process.env.RPC_URL || "https://rpc.orai.io").sync(); }; -sync(); +start(); -export { duckDb, sync }; +export { OraiDexSync }; diff --git a/packages/oraidex-sync/src/poc.ts b/packages/oraidex-sync/src/poc.ts new file mode 100644 index 00000000..92ba6bc8 --- /dev/null +++ b/packages/oraidex-sync/src/poc.ts @@ -0,0 +1,22 @@ +import { DuckDb } from "./db"; + +async function testAggregateLp() { + const duckDb = await DuckDb.create("oraidex-sync-data"); + let result = await duckDb.conn.all( + "select timestamp, firstTokenDenom, opType, sum(firstTokenAmount) as firstTokenAmount, if (opType ='provide', sum(firstTokenAmount), sum(firstTokenAmount) - sum(firstTokenAmount) * 2) as firstTokenAmount from lp_ops_data group by timestamp, firstTokenDenom, opType order by timestamp desc" + ); + console.log("results: ", result); + const currentLiquidity = 10000000000; + let prefixSumObj = {}; + for (let data of result) { + if (!(`temp-${data.firstTokenDenom}` in prefixSumObj)) { + prefixSumObj[`temp-${data.firstTokenDenom}`] = currentLiquidity + data.firstTokenAmount; + data.firstTokenAmount = prefixSumObj[`temp-${data.firstTokenDenom}`]; + continue; + } + prefixSumObj[`temp-${data.firstTokenDenom}`] += data.firstTokenAmount; + data.firstTokenAmount = prefixSumObj[`temp-${data.firstTokenDenom}`]; + } + console.log("new results: ", result); +} +testAggregateLp(); diff --git a/packages/oraidex-sync/src/tx-insert.ts b/packages/oraidex-sync/src/tx-insert.ts deleted file mode 100644 index 9a9840c4..00000000 --- a/packages/oraidex-sync/src/tx-insert.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { duckDb } from "../src"; -import { - ProvideLiquidityOperationData, - SwapOperationData, - TxAnlysisResult, - WithdrawLiquidityOperationData -} from "./types"; - -async function insertSwapOps(ops: SwapOperationData[]) { - await duckDb.insertSwapOps(ops); -} - -async function insertLiquidityOps(ops: ProvideLiquidityOperationData[] | WithdrawLiquidityOperationData[]) { - await duckDb.insertLpOps(ops); -} - -async function insertParsedTxs(txs: TxAnlysisResult) { - // insert swap ops - await Promise.all([ - insertSwapOps(txs.swapOpsData), - insertLiquidityOps(txs.provideLiquidityOpsData), - insertLiquidityOps(txs.withdrawLiquidityOpsData) - ]); -} - -export { insertParsedTxs }; diff --git a/packages/oraidex-sync/src/tx-query.ts b/packages/oraidex-sync/src/tx-query.ts deleted file mode 100644 index f52aa6c3..00000000 --- a/packages/oraidex-sync/src/tx-query.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { duckDb } from "."; - -async function querySwapOps() { - return duckDb.querySwapOps(); -} - -async function queryLpOps() { - return duckDb.queryLpOps(); -} - -export { queryLpOps, querySwapOps }; From 7412d4fe20877f95819f400f73ee9700c5724872 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 14 Jul 2023 17:11:24 +0700 Subject: [PATCH 19/75] added store pair info data --- packages/oraidex-sync/src/db.ts | 17 ++++++-- packages/oraidex-sync/src/index.ts | 54 ++++++++++++++++--------- packages/oraidex-sync/src/tx-parsing.ts | 5 ++- packages/oraidex-sync/src/types.ts | 9 +++++ 4 files changed, 62 insertions(+), 23 deletions(-) diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index f80ee19d..cede21e2 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -1,5 +1,5 @@ import { Database, Connection } from "duckdb-async"; -import { SwapOperationData, WithdrawLiquidityOperationData } from "./types"; +import { PairInfoData, SwapOperationData, WithdrawLiquidityOperationData } from "./types"; import fs from "fs"; export class DuckDb { @@ -17,11 +17,12 @@ export class DuckDb { async loadHeightSnapshot() { const result = await this.conn.all("SELECT * FROM height_snapshot"); - return result.length > 0 ? result[0] : { currentInd: 1 }; + console.log("result: ", result); + return result.length > 0 ? result[result.length - 1] : { currentInd: 1 }; } async insertHeightSnapshot(currentInd: number) { - await this.conn.run("INSERT OR REPLACE INTO height_snapshot VALUES (?)", currentInd); + await this.conn.run("INSERT OR REPLACE into height_snapshot(currentInd) VALUES (?)", currentInd); } private async insertBulkData(data: any[], tableName: string) { @@ -60,6 +61,16 @@ export class DuckDb { await this.insertBulkData(ops, "lp_ops_data"); } + async createPairInfosTable() { + await this.conn.exec( + "CREATE TABLE IF NOT EXISTS pair_infos (firstAssetInfo VARCHAR, secondAssetInfo VARCHAR, commissionRate VARCHAR, pairAddr VARCHAR, liquidityAddr VARCHAR, oracleAddr VARCHAR,PRIMARY KEY (pairAddr) )" + ); + } + + async insertPairInfos(ops: PairInfoData[]) { + await this.insertBulkData(ops, "pair_infos"); + } + async querySwapOps() { return this.conn.all("SELECT count(*) from swap_ops_data"); } diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 9f243e79..387d0d99 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -1,11 +1,12 @@ import "dotenv/config"; -import { parseTxs } from "./tx-parsing"; +import { parseAssetInfo, parseTxs } from "./tx-parsing"; import { DuckDb } from "./db"; import { WriteData, SyncData, Txs } from "@oraichain/cosmos-rpc-sync"; import "dotenv/config"; import { pairs } from "./pairs"; -import { CosmWasmClient, OraiswapFactoryQueryClient } from "@oraichain/oraidex-contracts-sdk"; +import { CosmWasmClient, OraiswapFactoryQueryClient, PairInfo } from "@oraichain/oraidex-contracts-sdk"; import { + PairInfoData, ProvideLiquidityOperationData, SwapOperationData, TxAnlysisResult, @@ -78,15 +79,18 @@ class OraiDexSync { cosmwasmClient, process.env.FACTORY_CONTACT_ADDRESS_V2 || "orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" ); - const liquidityResults = ( + const liquidityResults: PairInfo[] = ( await Promise.allSettled([ ...pairs.map((pair) => firstFactoryClient.pair({ assetInfos: pair.asset_infos })), ...pairs.map((pair) => secondFactoryClient.pair({ assetInfos: pair.asset_infos })) ]) - ).filter((res) => { - if (res.status === "fulfilled") return res.value; - }); - console.dir(liquidityResults, { depth: null }); + ) + .filter((res) => { + if (res.status === "fulfilled") return true; + return false; + }) + .map((data) => (data as any).value as PairInfo); + return liquidityResults; } public async sync() { @@ -94,24 +98,38 @@ class OraiDexSync { await Promise.all([ this.duckDb.createHeightSnapshot(), this.duckDb.createLiquidityOpsTable(), - this.duckDb.createSwapOpsTable() + this.duckDb.createSwapOpsTable(), + this.duckDb.createPairInfosTable() ]); - console.log("rpc url: ", this.rpcUrl); let { currentInd } = await this.duckDb.loadHeightSnapshot(); console.log("current ind: ", currentInd); // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic if (currentInd <= 12388825) { currentInd = 12388825; } - await this.getAllPairInfos(12389125); - // new SyncData({ - // offset: currentInd, - // rpcUrl, - // queryTags: [], - // limit: 100, - // maxThreadLevel: 3, - // interval: 5000 - // }).pipe(new WriteOrders()); + const pairInfos = await this.getAllPairInfos(); + // await this.duckDb.insertPairInfos( + // pairInfos.map( + // (pair) => + // ({ + // firstAssetInfo: parseAssetInfo(pair.asset_infos[0]), + // secondAssetInfo: parseAssetInfo(pair.asset_infos[1]), + // commissionRate: pair.commission_rate, + // pairAddr: pair.contract_addr, + // liquidityAddr: pair.liquidity_token, + // oracleAddr: pair.oracle_addr + // } as PairInfoData) + // ) + // ); + console.dir(pairInfos, { depth: null }); + new SyncData({ + offset: currentInd, + rpcUrl: this.rpcUrl, + queryTags: [], + limit: 100, + maxThreadLevel: 1, + interval: 5000 + }).pipe(new WriteOrders(this.duckDb)); } catch (error) { console.log("error in start: ", error); } diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 8f285f75..366ee55c 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -18,8 +18,9 @@ import { import { Log } from "@cosmjs/stargate/build/logs"; function parseAssetInfo(info: AssetInfo): string { - if ("native_token" in info) return info.native_token.denom; - return info.token.contract_addr; + // if ("native_token" in info) return info.native_token.denom; + // return info.token.contract_addr; + return JSON.stringify(info); } async function delay(timeout: number) { diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index a20734c5..0914a2dc 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -26,6 +26,15 @@ export type SwapOperationData = BasicTxData & { spreadAmount: number; }; +export type PairInfoData = { + firstAssetInfo: string; + secondAssetInfo: string; + commissionRate: string; + pairAddr: string; + liquidityAddr: string; + oracleAddr: string; +}; + export type AccountTx = { accountAddress: string; txhash: string; From fa27044760ffa2ee2a791c25b23c36a518578d32 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 14 Jul 2023 17:47:42 +0700 Subject: [PATCH 20/75] fixed snapshot height db structure --- packages/oraidex-sync/src/db.ts | 42 ++++++++++++++++++------------ packages/oraidex-sync/src/index.ts | 42 +++++++++++++++--------------- packages/oraidex-sync/src/poc.ts | 1 - 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index cede21e2..274d1ce6 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -11,30 +11,34 @@ export class DuckDb { return new DuckDb(conn); } + private async insertBulkData(data: any[], tableName: string, replace?: boolean) { + // we wont insert anything if the data is empty. Otherwise it would throw an error while inserting + if (data.length === 0) return; + const tableFile = `${tableName}.json`; + // the file written out is temporary only. Will be deleted after insertion + await fs.promises.writeFile(tableFile, JSON.stringify(data)); + const query = replace + ? `INSERT OR REPLACE INTO ${tableName} SELECT * FROM read_json_auto(?)` + : `INSERT INTO ${tableName} SELECT * FROM read_json_auto(?)`; + await this.conn.run(query, tableFile); + await fs.promises.unlink(tableFile); + } + + // sync height table async createHeightSnapshot() { - await this.conn.exec("CREATE TABLE IF NOT EXISTS height_snapshot (currentInd UINTEGER,PRIMARY KEY (currentInd))"); + await this.conn.exec("CREATE TABLE IF NOT EXISTS height_snapshot (config VARCHAR PRIMARY KEY, value UINTEGER)"); } async loadHeightSnapshot() { - const result = await this.conn.all("SELECT * FROM height_snapshot"); - console.log("result: ", result); - return result.length > 0 ? result[result.length - 1] : { currentInd: 1 }; + const result = await this.conn.all("SELECT value FROM height_snapshot where config = 'last_block_height'"); + return result.length > 0 ? result[0].value : { currentInd: 1 }; } async insertHeightSnapshot(currentInd: number) { - await this.conn.run("INSERT OR REPLACE into height_snapshot(currentInd) VALUES (?)", currentInd); - } - - private async insertBulkData(data: any[], tableName: string) { - // we wont insert anything if the data is empty. Otherwise it would throw an error while inserting - if (data.length === 0) return; - const tableFile = `${tableName}.json`; - // the file written out is temporary only. Will be deleted after insertion - await fs.promises.writeFile(tableFile, JSON.stringify(data)); - await this.conn.run(`INSERT INTO ${tableName} SELECT * FROM read_json_auto(?)`, tableFile); - await fs.promises.unlink(tableFile); + await this.conn.run("INSERT OR REPLACE into height_snapshot VALUES ('last_block_height',?)", currentInd); } + // swap operation table handling async createSwapOpsTable() { await this.conn.exec( "CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, timestamp TIMESTAMP, offerDenom VARCHAR, offerAmount UBIGINT, askDenom VARCHAR, returnAmount UBIGINT, taxAmount UBIGINT, commissionAmount UBIGINT, spreadAmount UBIGINT)" @@ -45,6 +49,7 @@ export class DuckDb { await this.insertBulkData(ops, "swap_ops_data"); } + // liquidity operations (provide, withdraw lp) handling async createLiquidityOpsTable() { try { await this.conn.all("select enum_range(NULL::LPOPTYPE);"); @@ -61,6 +66,7 @@ export class DuckDb { await this.insertBulkData(ops, "lp_ops_data"); } + // store all the current pair infos of oraiDEX. Will be updated to the latest pair list after the sync is restarted async createPairInfosTable() { await this.conn.exec( "CREATE TABLE IF NOT EXISTS pair_infos (firstAssetInfo VARCHAR, secondAssetInfo VARCHAR, commissionRate VARCHAR, pairAddr VARCHAR, liquidityAddr VARCHAR, oracleAddr VARCHAR,PRIMARY KEY (pairAddr) )" @@ -68,9 +74,13 @@ export class DuckDb { } async insertPairInfos(ops: PairInfoData[]) { - await this.insertBulkData(ops, "pair_infos"); + await this.insertBulkData(ops, "pair_infos", true); } + // we need to: + // price history should contain: timestamp, tx height, tx hash, asset info, price + // if cannot find then we spawn another stream and sync it started from the common sync height. We will re-sync it if its latest height is too behind compared to the common sync height + async querySwapOps() { return this.conn.all("SELECT count(*) from swap_ops_data"); } diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 387d0d99..6b3c52dd 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -35,12 +35,12 @@ class WriteOrders extends WriteData { ]); } - private async querySwapOps() { - return this.duckDb.querySwapOps(); + private async querySwapOps(): Promise { + return this.duckDb.querySwapOps() as Promise; } - private async queryLpOps() { - return this.duckDb.queryLpOps(); + private async queryLpOps(): Promise { + return this.duckDb.queryLpOps() as Promise; } async process(chunk: any): Promise { @@ -101,34 +101,34 @@ class OraiDexSync { this.duckDb.createSwapOpsTable(), this.duckDb.createPairInfosTable() ]); - let { currentInd } = await this.duckDb.loadHeightSnapshot(); + let currentInd = await this.duckDb.loadHeightSnapshot(); console.log("current ind: ", currentInd); // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic if (currentInd <= 12388825) { currentInd = 12388825; } const pairInfos = await this.getAllPairInfos(); - // await this.duckDb.insertPairInfos( - // pairInfos.map( - // (pair) => - // ({ - // firstAssetInfo: parseAssetInfo(pair.asset_infos[0]), - // secondAssetInfo: parseAssetInfo(pair.asset_infos[1]), - // commissionRate: pair.commission_rate, - // pairAddr: pair.contract_addr, - // liquidityAddr: pair.liquidity_token, - // oracleAddr: pair.oracle_addr - // } as PairInfoData) - // ) - // ); - console.dir(pairInfos, { depth: null }); + await this.duckDb.insertPairInfos( + pairInfos.map( + (pair) => + ({ + firstAssetInfo: parseAssetInfo(pair.asset_infos[0]), + secondAssetInfo: parseAssetInfo(pair.asset_infos[1]), + commissionRate: pair.commission_rate, + pairAddr: pair.contract_addr, + liquidityAddr: pair.liquidity_token, + oracleAddr: pair.oracle_addr + } as PairInfoData) + ) + ); + // console.dir(pairInfos, { depth: null }); new SyncData({ offset: currentInd, rpcUrl: this.rpcUrl, queryTags: [], - limit: 100, + limit: 1, maxThreadLevel: 1, - interval: 5000 + interval: 1000 }).pipe(new WriteOrders(this.duckDb)); } catch (error) { console.log("error in start: ", error); diff --git a/packages/oraidex-sync/src/poc.ts b/packages/oraidex-sync/src/poc.ts index 92ba6bc8..7bfb99aa 100644 --- a/packages/oraidex-sync/src/poc.ts +++ b/packages/oraidex-sync/src/poc.ts @@ -19,4 +19,3 @@ async function testAggregateLp() { } console.log("new results: ", result); } -testAggregateLp(); From eca029124e0895b12597edba9425814676c1cd77 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Sat, 15 Jul 2023 21:15:07 +0700 Subject: [PATCH 21/75] added helper functions for price cal with tcs & added scatom pair --- packages/oraidex-sync/.env.example | 4 +- packages/oraidex-sync/package.json | 4 ++ packages/oraidex-sync/src/constants.ts | 16 ++---- packages/oraidex-sync/src/db.ts | 19 +++++-- packages/oraidex-sync/src/helper.ts | 58 ++++++++++++++++++++ packages/oraidex-sync/src/index.ts | 27 ++++++++- packages/oraidex-sync/src/pairs.ts | 4 ++ packages/oraidex-sync/src/poc.ts | 21 ------- packages/oraidex-sync/src/tx-parsing.ts | 8 ++- packages/oraidex-sync/src/types.ts | 7 +++ packages/oraidex-sync/tests/db.spec.ts | 3 +- packages/oraidex-sync/tests/helper.spec.ts | 64 ++++++++++++++++++++++ yarn.lock | 10 ++++ 13 files changed, 205 insertions(+), 40 deletions(-) create mode 100644 packages/oraidex-sync/src/helper.ts delete mode 100644 packages/oraidex-sync/src/poc.ts create mode 100644 packages/oraidex-sync/tests/helper.spec.ts diff --git a/packages/oraidex-sync/.env.example b/packages/oraidex-sync/.env.example index 5ab5ed1e..aa19a167 100644 --- a/packages/oraidex-sync/.env.example +++ b/packages/oraidex-sync/.env.example @@ -1,3 +1,5 @@ RPC_URL=https://rpc.orai.io/ FACTORY_CONTACT_ADDRESS_V1="orai1hemdkz4xx9kukgrunxu3yw0nvpyxf34v82d2c8" -FACTORY_CONTACT_ADDRESS_V2="orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" \ No newline at end of file +FACTORY_CONTACT_ADDRESS_V2="orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" +ROUTER_CONTRACT_ADDRESS="orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" +MULTICALL_CONTRACT_ADDRESS="orai1q7x644gmf7h8u8y6y8t9z9nnwl8djkmspypr6mxavsk9ual7dj0sxpmgwd" \ No newline at end of file diff --git a/packages/oraidex-sync/package.json b/packages/oraidex-sync/package.json index d2d445f7..501f401e 100644 --- a/packages/oraidex-sync/package.json +++ b/packages/oraidex-sync/package.json @@ -12,8 +12,12 @@ }, "dependencies": { "@cosmjs/tendermint-rpc": "^0.31.0", + "@oraichain/common-contracts-sdk": "1.0.13", "@oraichain/cosmos-rpc-sync": "^1.0.5", "@oraichain/oraidex-contracts-sdk": "^1.0.13", "duckdb-async": "^0.8.1" + }, + "devDependencies": { + "@types/lodash": "^4.14.195" } } diff --git a/packages/oraidex-sync/src/constants.ts b/packages/oraidex-sync/src/constants.ts index 1b720403..e86b3354 100644 --- a/packages/oraidex-sync/src/constants.ts +++ b/packages/oraidex-sync/src/constants.ts @@ -4,13 +4,9 @@ export const oraixCw20Address = "orai1lus0f0rhx8s03gdllx2n6vhkmf0536dv57wfge"; export const usdtCw20Address = "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh"; export const kwtCw20Address = "orai1nd4r053e3kgedgld2ymen8l9yrw8xpjyaal7j5"; export const milkyCw20Address = "orai1gzvndtzceqwfymu2kqhta2jn6gmzxvzqwdgvjw"; -export const tronCw20Address = - "orai1c7tpjenafvgjtgm9aqwm7afnke6c56hpdms8jc6md40xs3ugd0es5encn0"; -export const scOraiCw20Address = - "orai1065qe48g7aemju045aeyprflytemx7kecxkf5m7u5h5mphd0qlcs47pclp"; -export const usdcCw20Address = - "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd"; -export const atomIbcDenom = - "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"; -export const osmosisIbcDenom = - "ibc/9C4DCD21B48231D0BC2AC3D1B74A864746B37E4292694C93C617324250D002FC"; +export const scAtomCw20Address = "orai19q4qak2g3cj2xc2y3060t0quzn3gfhzx08rjlrdd3vqxhjtat0cq668phq"; +export const tronCw20Address = "orai1c7tpjenafvgjtgm9aqwm7afnke6c56hpdms8jc6md40xs3ugd0es5encn0"; +export const scOraiCw20Address = "orai1065qe48g7aemju045aeyprflytemx7kecxkf5m7u5h5mphd0qlcs47pclp"; +export const usdcCw20Address = "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd"; +export const atomIbcDenom = "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"; +export const osmosisIbcDenom = "ibc/9C4DCD21B48231D0BC2AC3D1B74A864746B37E4292694C93C617324250D002FC"; diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 274d1ce6..00c7b046 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -1,5 +1,5 @@ import { Database, Connection } from "duckdb-async"; -import { PairInfoData, SwapOperationData, WithdrawLiquidityOperationData } from "./types"; +import { PairInfoData, PriceInfo, SwapOperationData, WithdrawLiquidityOperationData } from "./types"; import fs from "fs"; export class DuckDb { @@ -11,10 +11,10 @@ export class DuckDb { return new DuckDb(conn); } - private async insertBulkData(data: any[], tableName: string, replace?: boolean) { + private async insertBulkData(data: any[], tableName: string, replace?: boolean, fileName?: string) { // we wont insert anything if the data is empty. Otherwise it would throw an error while inserting if (data.length === 0) return; - const tableFile = `${tableName}.json`; + const tableFile = fileName ?? `${tableName}.json`; // the file written out is temporary only. Will be deleted after insertion await fs.promises.writeFile(tableFile, JSON.stringify(data)); const query = replace @@ -78,8 +78,19 @@ export class DuckDb { } // we need to: - // price history should contain: timestamp, tx height, tx hash, asset info, price + // price history should contain: timestamp, tx height, asset info, price // if cannot find then we spawn another stream and sync it started from the common sync height. We will re-sync it if its latest height is too behind compared to the common sync height + // if + + async createPriceInfoTable() { + await this.conn.exec( + "CREATE TABLE IF NOT EXISTS price_infos (txheight VARCHAR, timestamp TIMESTAMP, assetInfo VARCHAR, price FLOAT4)" + ); + } + + async insertPriceInfos(ops: PriceInfo[]) { + await this.insertBulkData(ops, "price_infos", false, `price_infos-${Math.random() * 1000}`); + } async querySwapOps() { return this.conn.all("SELECT count(*) from swap_ops_data"); diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts new file mode 100644 index 00000000..2d68f260 --- /dev/null +++ b/packages/oraidex-sync/src/helper.ts @@ -0,0 +1,58 @@ +import { AssetInfo, OraiswapRouterReadOnlyInterface, SwapOperation } from "@oraichain/oraidex-contracts-sdk"; +import { parseAssetInfo, parseAssetInfoOnlyDenom } from "./tx-parsing"; +import { pairs } from "./pairs"; +import { isEqual, map } from "lodash"; +import { ORAI, usdtCw20Address } from "./constants"; + +export type PrefixSumHandlingData = { + denom: string; + amount: number; +}; + +function calculatePrefixSum(initialAmount: number, handlingData: PrefixSumHandlingData[]): PrefixSumHandlingData[] { + let prefixSumObj = {}; + for (let data of handlingData) { + if (!(`temp-${data.denom}` in prefixSumObj)) { + prefixSumObj[`temp-${data.denom}`] = initialAmount + data.amount; + data.amount = prefixSumObj[`temp-${data.denom}`]; + continue; + } + prefixSumObj[`temp-${data.denom}`] += data.amount; + data.amount = prefixSumObj[`temp-${data.denom}`]; + } + console.log("new results: ", handlingData); + return handlingData; +} + +function findMappedTargetedAssetInfo(targetedAssetInfo: AssetInfo): AssetInfo[] { + const mappedAssetInfos = []; + + for (const pair of pairs) { + const infos = pair.asset_infos; + if (parseAssetInfo(infos[0]) === parseAssetInfo(targetedAssetInfo)) mappedAssetInfos.push(infos[1]); + else if (parseAssetInfo(infos[1]) === parseAssetInfo(targetedAssetInfo)) mappedAssetInfos.push(infos[0]); + else continue; + } + + return mappedAssetInfos; +} + +function findAssetInfoPathToUsdt(info: AssetInfo): AssetInfo[] { + // first, check usdt mapped target infos because if we the info pairs with usdt directly then we can easily calculate its price + // otherwise, we find orai mapped target infos, which can lead to usdt. + // finally, if not paired with orai, then we find recusirvely to find a path leading to usdt token + const usdtInfo = { token: { contract_addr: usdtCw20Address } }; + const oraiInfo = { native_token: { denom: ORAI } }; + if (parseAssetInfo(info) === parseAssetInfo(usdtInfo)) return [info]; // means there's no path, the price should be 1 + const mappedUsdtInfoList = findMappedTargetedAssetInfo(usdtInfo); + if (mappedUsdtInfoList.find((assetInfo) => parseAssetInfo(assetInfo) === parseAssetInfo(info))) + return [info, usdtInfo]; + const mappedOraiInfoList = findMappedTargetedAssetInfo(oraiInfo); + if (mappedOraiInfoList.find((assetInfo) => parseAssetInfo(assetInfo) === parseAssetInfo(info))) + return [info, oraiInfo, usdtInfo]; + const pairedInfo = findMappedTargetedAssetInfo(info); + if (pairedInfo.length === 0) return []; // cannot find any mapped target pair + return [info, ...findAssetInfoPathToUsdt(pairedInfo[0])]; // only need the first found paired token with the one we are matching +} + +export { calculatePrefixSum, findMappedTargetedAssetInfo, findAssetInfoPathToUsdt }; diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 6b3c52dd..2318d747 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -12,6 +12,9 @@ import { TxAnlysisResult, WithdrawLiquidityOperationData } from "./types"; +import { MulticallQueryClient } from "@oraichain/common-contracts-sdk"; +import { fromBinary, toBinary } from "@cosmjs/cosmwasm-stargate"; +import { PoolResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; class WriteOrders extends WriteData { constructor(private duckDb: DuckDb) { @@ -68,9 +71,28 @@ class WriteOrders extends WriteData { class OraiDexSync { constructor(private duckDb: DuckDb, private rpcUrl: string) {} - private async getAllPairInfos(wantedHeight?: number) { + private async getPoolInfos(pairs: PairInfo[], wantedHeight?: number): Promise { const cosmwasmClient = await CosmWasmClient.connect(this.rpcUrl); cosmwasmClient.setQueryClientWithHeight(wantedHeight); + const multicall = new MulticallQueryClient( + cosmwasmClient, + process.env.MULTICALL_CONTRACT_ADDRES || "orai1q7x644gmf7h8u8y6y8t9z9nnwl8djkmspypr6mxavsk9ual7dj0sxpmgwd" + ); + const res = await multicall.tryAggregate({ + queries: pairs.map((pair) => { + return { + address: pair.contract_addr, + data: toBinary({ + pool: {} + }) + }; + }) + }); + return res.return_data.map((data) => (data.success ? fromBinary(data.data) : undefined)); + } + + private async getAllPairInfos(): Promise { + const cosmwasmClient = await CosmWasmClient.connect(this.rpcUrl); const firstFactoryClient = new OraiswapFactoryQueryClient( cosmwasmClient, process.env.FACTORY_CONTACT_ADDRESS_V1 || "orai1hemdkz4xx9kukgrunxu3yw0nvpyxf34v82d2c8" @@ -108,6 +130,9 @@ class OraiDexSync { currentInd = 12388825; } const pairInfos = await this.getAllPairInfos(); + // TODO: only get pool infos of selected pairs if that pair does not exist in the pair info database, meaning it is new. Otherwise, it would have been called before and stored the pool result given the wanted height. + const poolResultsAtOldHeight = await this.getPoolInfos(pairInfos, currentInd); + // Promise.all([insert pool info, and insert pair info. Promise all because pool info & updated pair info must go together]) await this.duckDb.insertPairInfos( pairInfos.map( (pair) => diff --git a/packages/oraidex-sync/src/pairs.ts b/packages/oraidex-sync/src/pairs.ts index 3846b9e5..a7810f3b 100644 --- a/packages/oraidex-sync/src/pairs.ts +++ b/packages/oraidex-sync/src/pairs.ts @@ -8,6 +8,7 @@ import { milkyCw20Address, oraixCw20Address, osmosisIbcDenom, + scAtomCw20Address, scOraiCw20Address, tronCw20Address, usdcCw20Address, @@ -50,5 +51,8 @@ export const pairs: PairMapping[] = [ }, { asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: tronCw20Address } }] + }, + { + asset_infos: [{ native_token: { denom: atomIbcDenom } }, { token: { contract_addr: scAtomCw20Address } }] } ]; diff --git a/packages/oraidex-sync/src/poc.ts b/packages/oraidex-sync/src/poc.ts deleted file mode 100644 index 7bfb99aa..00000000 --- a/packages/oraidex-sync/src/poc.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DuckDb } from "./db"; - -async function testAggregateLp() { - const duckDb = await DuckDb.create("oraidex-sync-data"); - let result = await duckDb.conn.all( - "select timestamp, firstTokenDenom, opType, sum(firstTokenAmount) as firstTokenAmount, if (opType ='provide', sum(firstTokenAmount), sum(firstTokenAmount) - sum(firstTokenAmount) * 2) as firstTokenAmount from lp_ops_data group by timestamp, firstTokenDenom, opType order by timestamp desc" - ); - console.log("results: ", result); - const currentLiquidity = 10000000000; - let prefixSumObj = {}; - for (let data of result) { - if (!(`temp-${data.firstTokenDenom}` in prefixSumObj)) { - prefixSumObj[`temp-${data.firstTokenDenom}`] = currentLiquidity + data.firstTokenAmount; - data.firstTokenAmount = prefixSumObj[`temp-${data.firstTokenDenom}`]; - continue; - } - prefixSumObj[`temp-${data.firstTokenDenom}`] += data.firstTokenAmount; - data.firstTokenAmount = prefixSumObj[`temp-${data.firstTokenDenom}`]; - } - console.log("new results: ", result); -} diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 366ee55c..d37f7491 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -23,6 +23,11 @@ function parseAssetInfo(info: AssetInfo): string { return JSON.stringify(info); } +function parseAssetInfoOnlyDenom(info: AssetInfo): string { + if ("native_token" in info) return info.native_token.denom; + return info.token.contract_addr; +} + async function delay(timeout: number) { return new Promise((resolve) => setTimeout(resolve, timeout)); } @@ -232,5 +237,6 @@ export { parseWasmEvents, parseTxs, parseWithdrawLiquidityAssets, - parseTxToMsgExecuteContractMsgs + parseTxToMsgExecuteContractMsgs, + parseAssetInfoOnlyDenom }; diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 0914a2dc..224a1c29 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -35,6 +35,13 @@ export type PairInfoData = { oracleAddr: string; }; +export type PriceInfo = { + txheight: string; + timestamp: string; + assetInfo: string; + price: number; +}; + export type AccountTx = { accountAddress: string; txhash: string; diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index 92704742..75bccad1 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -4,8 +4,7 @@ describe("test-duckdb", () => { let duckDb: DuckDb; beforeAll(async () => { // fixture - duckDb = new DuckDb(); - await duckDb.initDuckDb("oraidex-sync-data-test"); + duckDb = await DuckDb.create("oraidex-sync-data-test"); await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); }); diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts new file mode 100644 index 00000000..5005fb67 --- /dev/null +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -0,0 +1,64 @@ +import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; +import { calculatePrefixSum, findAssetInfoPathToUsdt, findMappedTargetedAssetInfo } from "../src/helper"; +import { pairs } from "../src/pairs"; +import { + airiCw20Adress, + milkyCw20Address, + scAtomCw20Address, + usdcCw20Address, + usdtCw20Address +} from "../src/constants"; + +describe("test-helper", () => { + it.each<[AssetInfo, number]>([ + [{ token: { contract_addr: usdtCw20Address } }, 2], + [{ token: { contract_addr: usdcCw20Address } }, 1], + [{ native_token: { denom: "orai" } }, 9], + [{ token: { contract_addr: airiCw20Adress } }, 1] + ])("test-findMappedTargetedAssetInfo", (info, expectedListLength) => { + // setup + + // act + const result = findMappedTargetedAssetInfo(info); + + // assert + expect(result.length).toEqual(expectedListLength); + }); + + it.each<[AssetInfo, number]>([ + [{ token: { contract_addr: usdtCw20Address } }, 1], + [{ native_token: { denom: "orai" } }, 2], + [{ token: { contract_addr: airiCw20Adress } }, 3], + [{ token: { contract_addr: milkyCw20Address } }, 2], + [{ token: { contract_addr: scAtomCw20Address } }, 4] + ])("test-findAssetInfoPathToUsdt", (info, expectedListLength) => { + // setup + + // act + const result = findAssetInfoPathToUsdt(info); + + // assert + expect(result.length).toEqual(expectedListLength); + }); + + it("test-calculatePrefixSum", () => { + const data = [ + { + denom: "foo", + amount: 100 + }, + { denom: "foo", amount: 10 }, + { denom: "bar", amount: 5 }, + { denom: "bar", amount: -1 }, + { denom: "hello", amount: 5 } + ]; + const result = calculatePrefixSum(1, data); + expect(result).toEqual([ + { denom: "foo", amount: 101 }, + { denom: "foo", amount: 111 }, + { denom: "bar", amount: 6 }, + { denom: "bar", amount: 5 }, + { denom: "hello", amount: 6 } + ]); + }); +}); diff --git a/yarn.lock b/yarn.lock index c74a5613..189b8bef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1940,6 +1940,11 @@ dependencies: "@octokit/openapi-types" "^17.1.1" +"@oraichain/common-contracts-sdk@1.0.13": + version "1.0.13" + resolved "https://registry.yarnpkg.com/@oraichain/common-contracts-sdk/-/common-contracts-sdk-1.0.13.tgz#5a6cd9320b995e184b5d484313e4b4e8ecd4b4bb" + integrity sha512-XfDDaggu7WcM/vxRxIn0ipLq6+c9+5FBqK/qzWh5HRHxn4e71OvNKVAxXoLOIATscAXkrkOimv55s1CD+hZGmw== + "@oraichain/cosmos-rpc-sync@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@oraichain/cosmos-rpc-sync/-/cosmos-rpc-sync-1.0.5.tgz#252e2434c3fe1a2e82793cf42b842e9d939e3aa1" @@ -2159,6 +2164,11 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/lodash@^4.14.195": + version "4.14.195" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" + integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== + "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" From 8354980747a1860ea815337d3634c17257823117 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Sun, 16 Jul 2023 19:54:09 +0700 Subject: [PATCH 22/75] simulated token price at a specific height --- packages/oraidex-sync/src/constants.ts | 1 + packages/oraidex-sync/src/helper.ts | 38 ++++++- packages/oraidex-sync/src/index.ts | 117 +++++++++++++++------ packages/oraidex-sync/tests/helper.spec.ts | 39 ++++++- 4 files changed, 158 insertions(+), 37 deletions(-) diff --git a/packages/oraidex-sync/src/constants.ts b/packages/oraidex-sync/src/constants.ts index e86b3354..5d6a070c 100644 --- a/packages/oraidex-sync/src/constants.ts +++ b/packages/oraidex-sync/src/constants.ts @@ -10,3 +10,4 @@ export const scOraiCw20Address = "orai1065qe48g7aemju045aeyprflytemx7kecxkf5m7u5 export const usdcCw20Address = "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd"; export const atomIbcDenom = "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"; export const osmosisIbcDenom = "ibc/9C4DCD21B48231D0BC2AC3D1B74A864746B37E4292694C93C617324250D002FC"; +export const tenAmountInDecimalSix = "10000000"; diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index 2d68f260..dc70a1a9 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -1,8 +1,9 @@ -import { AssetInfo, OraiswapRouterReadOnlyInterface, SwapOperation } from "@oraichain/oraidex-contracts-sdk"; +import { Asset, AssetInfo, OraiswapRouterReadOnlyInterface, SwapOperation } from "@oraichain/oraidex-contracts-sdk"; import { parseAssetInfo, parseAssetInfoOnlyDenom } from "./tx-parsing"; import { pairs } from "./pairs"; import { isEqual, map } from "lodash"; import { ORAI, usdtCw20Address } from "./constants"; +import { PairMapping } from "./types"; export type PrefixSumHandlingData = { denom: string; @@ -55,4 +56,37 @@ function findAssetInfoPathToUsdt(info: AssetInfo): AssetInfo[] { return [info, ...findAssetInfoPathToUsdt(pairedInfo[0])]; // only need the first found paired token with the one we are matching } -export { calculatePrefixSum, findMappedTargetedAssetInfo, findAssetInfoPathToUsdt }; +function generateSwapOperations(info: AssetInfo): SwapOperation[] { + const infoPath = findAssetInfoPathToUsdt(info); + let swapOps: SwapOperation[] = []; + for (let i = 0; i < infoPath.length - 1; i++) { + swapOps.push({ orai_swap: { offer_asset_info: infoPath[i], ask_asset_info: infoPath[i + 1] } } as SwapOperation); + } + return swapOps; +} + +function extractUniqueAndFlatten(data: PairMapping[]): AssetInfo[] { + const uniqueItems = new Set(); + + data.forEach((item) => { + item.asset_infos.forEach((info) => { + const stringValue = JSON.stringify(info); + + if (!uniqueItems.has(stringValue)) { + uniqueItems.add(stringValue); + } + }); + }); + + const uniqueFlattenedArray = Array.from(uniqueItems).map((item) => JSON.parse(item as string)); + + return uniqueFlattenedArray; +} + +export { + calculatePrefixSum, + findMappedTargetedAssetInfo, + findAssetInfoPathToUsdt, + generateSwapOperations, + extractUniqueAndFlatten +}; diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 2318d747..8de8a466 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -4,7 +4,14 @@ import { DuckDb } from "./db"; import { WriteData, SyncData, Txs } from "@oraichain/cosmos-rpc-sync"; import "dotenv/config"; import { pairs } from "./pairs"; -import { CosmWasmClient, OraiswapFactoryQueryClient, PairInfo } from "@oraichain/oraidex-contracts-sdk"; +import { + AssetInfo, + CosmWasmClient, + OraiswapFactoryQueryClient, + OraiswapRouterQueryClient, + PairInfo, + SwapOperation +} from "@oraichain/oraidex-contracts-sdk"; import { PairInfoData, ProvideLiquidityOperationData, @@ -15,6 +22,8 @@ import { import { MulticallQueryClient } from "@oraichain/common-contracts-sdk"; import { fromBinary, toBinary } from "@cosmjs/cosmwasm-stargate"; import { PoolResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; +import { extractUniqueAndFlatten, findAssetInfoPathToUsdt, generateSwapOperations } from "./helper"; +import { tenAmountInDecimalSix } from "./constants"; class WriteOrders extends WriteData { constructor(private duckDb: DuckDb) { @@ -69,13 +78,18 @@ class WriteOrders extends WriteData { } class OraiDexSync { - constructor(private duckDb: DuckDb, private rpcUrl: string) {} + protected constructor(private duckDb: DuckDb, private rpcUrl: string, private cosmwasmClient: CosmWasmClient) {} + + public static async create(duckDb: DuckDb, rpcUrl: string): Promise { + const cosmwasmClient = await CosmWasmClient.connect(rpcUrl); + return new OraiDexSync(duckDb, rpcUrl, cosmwasmClient); + } private async getPoolInfos(pairs: PairInfo[], wantedHeight?: number): Promise { - const cosmwasmClient = await CosmWasmClient.connect(this.rpcUrl); - cosmwasmClient.setQueryClientWithHeight(wantedHeight); + // adjust the query height to get data from the past + this.cosmwasmClient.setQueryClientWithHeight(wantedHeight); const multicall = new MulticallQueryClient( - cosmwasmClient, + this.cosmwasmClient, process.env.MULTICALL_CONTRACT_ADDRES || "orai1q7x644gmf7h8u8y6y8t9z9nnwl8djkmspypr6mxavsk9ual7dj0sxpmgwd" ); const res = await multicall.tryAggregate({ @@ -88,17 +102,18 @@ class OraiDexSync { }; }) }); + // reset query client to latest for other functions to call + this.cosmwasmClient.setQueryClientWithHeight(); return res.return_data.map((data) => (data.success ? fromBinary(data.data) : undefined)); } private async getAllPairInfos(): Promise { - const cosmwasmClient = await CosmWasmClient.connect(this.rpcUrl); const firstFactoryClient = new OraiswapFactoryQueryClient( - cosmwasmClient, + this.cosmwasmClient, process.env.FACTORY_CONTACT_ADDRESS_V1 || "orai1hemdkz4xx9kukgrunxu3yw0nvpyxf34v82d2c8" ); const secondFactoryClient = new OraiswapFactoryQueryClient( - cosmwasmClient, + this.cosmwasmClient, process.env.FACTORY_CONTACT_ADDRESS_V2 || "orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" ); const liquidityResults: PairInfo[] = ( @@ -115,6 +130,34 @@ class OraiDexSync { return liquidityResults; } + private async simulateSwapPrice(info: AssetInfo, wantedHeight?: number): Promise { + // adjust the query height to get data from the past + this.cosmwasmClient.setQueryClientWithHeight(wantedHeight); + const infoPath = findAssetInfoPathToUsdt(info); + // usdt case, price is always 1 + if (infoPath.length === 1) return tenAmountInDecimalSix.substring(0, tenAmountInDecimalSix.length - 1); + const operations = generateSwapOperations(info); + if (operations.length === 0) return "0"; // error case. Will be handled by the caller function + const routerContract = new OraiswapRouterQueryClient( + this.cosmwasmClient, + process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" + ); + + try { + const data = await routerContract.simulateSwapOperations({ + offerAmount: tenAmountInDecimalSix, + operations + }); + // reset query client to latest for other functions to call. + this.cosmwasmClient.setQueryClientWithHeight(); + return data.amount.substring(0, data.amount.length - 1); // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit + } catch (error) { + throw new Error( + `Error when trying to simulate swap with asset info: ${JSON.stringify(info)} using router: ${error}` + ); + } + } + public async sync() { try { await Promise.all([ @@ -129,32 +172,37 @@ class OraiDexSync { if (currentInd <= 12388825) { currentInd = 12388825; } - const pairInfos = await this.getAllPairInfos(); - // TODO: only get pool infos of selected pairs if that pair does not exist in the pair info database, meaning it is new. Otherwise, it would have been called before and stored the pool result given the wanted height. - const poolResultsAtOldHeight = await this.getPoolInfos(pairInfos, currentInd); - // Promise.all([insert pool info, and insert pair info. Promise all because pool info & updated pair info must go together]) - await this.duckDb.insertPairInfos( - pairInfos.map( - (pair) => - ({ - firstAssetInfo: parseAssetInfo(pair.asset_infos[0]), - secondAssetInfo: parseAssetInfo(pair.asset_infos[1]), - commissionRate: pair.commission_rate, - pairAddr: pair.contract_addr, - liquidityAddr: pair.liquidity_token, - oracleAddr: pair.oracle_addr - } as PairInfoData) - ) + + const tokenPrices = await Promise.all( + extractUniqueAndFlatten(pairs).map((info) => this.simulateSwapPrice(info, currentInd)) ); - // console.dir(pairInfos, { depth: null }); - new SyncData({ - offset: currentInd, - rpcUrl: this.rpcUrl, - queryTags: [], - limit: 1, - maxThreadLevel: 1, - interval: 1000 - }).pipe(new WriteOrders(this.duckDb)); + console.log("token prices: ", tokenPrices); + // const pairInfos = await this.getAllPairInfos(); + // // TODO: only get pool infos of selected pairs if that pair does not exist in the pair info database, meaning it is new. Otherwise, it would have been called before and stored the pool result given the wanted height. + // const poolResultsAtOldHeight = await this.getPoolInfos(pairInfos, currentInd); + // // Promise.all([insert pool info, and insert pair info. Promise all because pool info & updated pair info must go together]) + // await this.duckDb.insertPairInfos( + // pairInfos.map( + // (pair) => + // ({ + // firstAssetInfo: parseAssetInfo(pair.asset_infos[0]), + // secondAssetInfo: parseAssetInfo(pair.asset_infos[1]), + // commissionRate: pair.commission_rate, + // pairAddr: pair.contract_addr, + // liquidityAddr: pair.liquidity_token, + // oracleAddr: pair.oracle_addr + // } as PairInfoData) + // ) + // ); + // // console.dir(pairInfos, { depth: null }); + // new SyncData({ + // offset: currentInd, + // rpcUrl: this.rpcUrl, + // queryTags: [], + // limit: 1, + // maxThreadLevel: 1, + // interval: 1000 + // }).pipe(new WriteOrders(this.duckDb)); } catch (error) { console.log("error in start: ", error); } @@ -163,7 +211,8 @@ class OraiDexSync { const start = async () => { const duckDb = await DuckDb.create("oraidex-sync-data"); - new OraiDexSync(duckDb, process.env.RPC_URL || "https://rpc.orai.io").sync(); + const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); + await oraidexSync.sync(); }; start(); diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index 5005fb67..4196664a 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -1,5 +1,10 @@ import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; -import { calculatePrefixSum, findAssetInfoPathToUsdt, findMappedTargetedAssetInfo } from "../src/helper"; +import { + calculatePrefixSum, + findAssetInfoPathToUsdt, + findMappedTargetedAssetInfo, + extractUniqueAndFlatten +} from "../src/helper"; import { pairs } from "../src/pairs"; import { airiCw20Adress, @@ -61,4 +66,36 @@ describe("test-helper", () => { { denom: "hello", amount: 6 } ]); }); + + it("test-extractUniqueAndFlatten-extracting-unique-items-in-pair-mapping", () => { + // act + const result = extractUniqueAndFlatten(pairs); + // assert + expect(result).toEqual([ + { native_token: { denom: "orai" } }, + { token: { contract_addr: "orai10ldgzued6zjp0mkqwsv2mux3ml50l97c74x8sg" } }, + { token: { contract_addr: "orai1lus0f0rhx8s03gdllx2n6vhkmf0536dv57wfge" } }, + { + token: { contract_addr: "orai1065qe48g7aemju045aeyprflytemx7kecxkf5m7u5h5mphd0qlcs47pclp" } + }, + { + native_token: { denom: "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78" } + }, + { token: { contract_addr: "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh" } }, + { token: { contract_addr: "orai1nd4r053e3kgedgld2ymen8l9yrw8xpjyaal7j5" } }, + { + native_token: { denom: "ibc/9C4DCD21B48231D0BC2AC3D1B74A864746B37E4292694C93C617324250D002FC" } + }, + { token: { contract_addr: "orai1gzvndtzceqwfymu2kqhta2jn6gmzxvzqwdgvjw" } }, + { + token: { contract_addr: "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd" } + }, + { + token: { contract_addr: "orai1c7tpjenafvgjtgm9aqwm7afnke6c56hpdms8jc6md40xs3ugd0es5encn0" } + }, + { + token: { contract_addr: "orai19q4qak2g3cj2xc2y3060t0quzn3gfhzx08rjlrdd3vqxhjtat0cq668phq" } + } + ]); + }); }); From c259ea586c4ed05af9b7872eb684661cc3145f1b Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Sun, 16 Jul 2023 22:35:28 +0700 Subject: [PATCH 23/75] added insert price infos first write when running sync --- packages/oraidex-sync/.env.example | 2 +- packages/oraidex-sync/src/db.ts | 4 +- packages/oraidex-sync/src/helper.ts | 8 +--- packages/oraidex-sync/src/index.ts | 64 +++++++++++++++++++++-------- packages/oraidex-sync/src/types.ts | 13 +++++- 5 files changed, 62 insertions(+), 29 deletions(-) diff --git a/packages/oraidex-sync/.env.example b/packages/oraidex-sync/.env.example index aa19a167..ce06d7c9 100644 --- a/packages/oraidex-sync/.env.example +++ b/packages/oraidex-sync/.env.example @@ -1,4 +1,4 @@ -RPC_URL=https://rpc.orai.io/ +RPC_URL=http://35.237.59.125:26657 FACTORY_CONTACT_ADDRESS_V1="orai1hemdkz4xx9kukgrunxu3yw0nvpyxf34v82d2c8" FACTORY_CONTACT_ADDRESS_V2="orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" ROUTER_CONTRACT_ADDRESS="orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 00c7b046..7cc24d1f 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -80,11 +80,11 @@ export class DuckDb { // we need to: // price history should contain: timestamp, tx height, asset info, price // if cannot find then we spawn another stream and sync it started from the common sync height. We will re-sync it if its latest height is too behind compared to the common sync height - // if + // if async createPriceInfoTable() { await this.conn.exec( - "CREATE TABLE IF NOT EXISTS price_infos (txheight VARCHAR, timestamp TIMESTAMP, assetInfo VARCHAR, price FLOAT4)" + "CREATE TABLE IF NOT EXISTS price_infos (txheight UINTEGER, timestamp TIMESTAMP, assetInfo VARCHAR, price UINTEGER)" ); } diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index dc70a1a9..b7432ecc 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -1,14 +1,8 @@ import { Asset, AssetInfo, OraiswapRouterReadOnlyInterface, SwapOperation } from "@oraichain/oraidex-contracts-sdk"; import { parseAssetInfo, parseAssetInfoOnlyDenom } from "./tx-parsing"; import { pairs } from "./pairs"; -import { isEqual, map } from "lodash"; import { ORAI, usdtCw20Address } from "./constants"; -import { PairMapping } from "./types"; - -export type PrefixSumHandlingData = { - denom: string; - amount: number; -}; +import { PairMapping, PrefixSumHandlingData } from "./types"; function calculatePrefixSum(initialAmount: number, handlingData: PrefixSumHandlingData[]): PrefixSumHandlingData[] { let prefixSumObj = {}; diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 8de8a466..8d4d753d 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -5,6 +5,7 @@ import { WriteData, SyncData, Txs } from "@oraichain/cosmos-rpc-sync"; import "dotenv/config"; import { pairs } from "./pairs"; import { + Asset, AssetInfo, CosmWasmClient, OraiswapFactoryQueryClient, @@ -17,7 +18,9 @@ import { ProvideLiquidityOperationData, SwapOperationData, TxAnlysisResult, - WithdrawLiquidityOperationData + WithdrawLiquidityOperationData, + InitialData, + PriceInfo } from "./types"; import { MulticallQueryClient } from "@oraichain/common-contracts-sdk"; import { fromBinary, toBinary } from "@cosmjs/cosmwasm-stargate"; @@ -26,8 +29,10 @@ import { extractUniqueAndFlatten, findAssetInfoPathToUsdt, generateSwapOperation import { tenAmountInDecimalSix } from "./constants"; class WriteOrders extends WriteData { - constructor(private duckDb: DuckDb) { + private firstWrite: boolean; + constructor(private duckDb: DuckDb, private initialData: InitialData) { super(); + this.firstWrite = true; } private async insertSwapOps(ops: SwapOperationData[]) { @@ -57,6 +62,24 @@ class WriteOrders extends WriteData { async process(chunk: any): Promise { try { + // first time calling of the application then we query past data and be ready to store them into the db for prefix sum + // this helps the flow go smoothly and remove dependency between different streams + if (this.firstWrite) { + console.log("initial data: ", this.initialData); + const { height, time } = this.initialData.blockHeader; + await this.duckDb.insertPriceInfos( + this.initialData.tokenPrices.map( + (tokenPrice) => + ({ + txheight: height, + timestamp: time, + assetInfo: parseAssetInfo(tokenPrice.info), + price: parseInt(tokenPrice.amount) + } as PriceInfo) + ) + ); + this.firstWrite = false; + } const { txs, offset: newOffset, queryTags } = chunk as Txs; console.log("new offset: ", newOffset); const result = parseTxs(txs); @@ -130,14 +153,15 @@ class OraiDexSync { return liquidityResults; } - private async simulateSwapPrice(info: AssetInfo, wantedHeight?: number): Promise { + private async simulateSwapPrice(info: AssetInfo, wantedHeight?: number): Promise { // adjust the query height to get data from the past this.cosmwasmClient.setQueryClientWithHeight(wantedHeight); const infoPath = findAssetInfoPathToUsdt(info); // usdt case, price is always 1 - if (infoPath.length === 1) return tenAmountInDecimalSix.substring(0, tenAmountInDecimalSix.length - 1); + if (infoPath.length === 1) + return { info, amount: tenAmountInDecimalSix.substring(0, tenAmountInDecimalSix.length - 1) }; const operations = generateSwapOperations(info); - if (operations.length === 0) return "0"; // error case. Will be handled by the caller function + if (operations.length === 0) return { info, amount: "0" }; // error case. Will be handled by the caller function const routerContract = new OraiswapRouterQueryClient( this.cosmwasmClient, process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" @@ -150,11 +174,12 @@ class OraiDexSync { }); // reset query client to latest for other functions to call. this.cosmwasmClient.setQueryClientWithHeight(); - return data.amount.substring(0, data.amount.length - 1); // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit + return { info, amount: data.amount.substring(0, data.amount.length - 1) }; // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit } catch (error) { - throw new Error( - `Error when trying to simulate swap with asset info: ${JSON.stringify(info)} using router: ${error}` - ); + console.log(`Error when trying to simulate swap with asset info: ${JSON.stringify(info)} using router: ${error}`); + // reset query client to latest for other functions to call. + this.cosmwasmClient.setQueryClientWithHeight(); + return { info, amount: "0" }; // error case. Will be handled by the caller function } } @@ -167,6 +192,7 @@ class OraiDexSync { this.duckDb.createPairInfosTable() ]); let currentInd = await this.duckDb.loadHeightSnapshot(); + let initialData: InitialData = { tokenPrices: [], blockHeader: undefined }; console.log("current ind: ", currentInd); // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic if (currentInd <= 12388825) { @@ -176,7 +202,9 @@ class OraiDexSync { const tokenPrices = await Promise.all( extractUniqueAndFlatten(pairs).map((info) => this.simulateSwapPrice(info, currentInd)) ); - console.log("token prices: ", tokenPrices); + const initialBlockHeader = (await this.cosmwasmClient.getBlock(currentInd)).header; + initialData.tokenPrices = tokenPrices; + initialData.blockHeader = initialBlockHeader; // const pairInfos = await this.getAllPairInfos(); // // TODO: only get pool infos of selected pairs if that pair does not exist in the pair info database, meaning it is new. Otherwise, it would have been called before and stored the pool result given the wanted height. // const poolResultsAtOldHeight = await this.getPoolInfos(pairInfos, currentInd); @@ -195,14 +223,14 @@ class OraiDexSync { // ) // ); // // console.dir(pairInfos, { depth: null }); - // new SyncData({ - // offset: currentInd, - // rpcUrl: this.rpcUrl, - // queryTags: [], - // limit: 1, - // maxThreadLevel: 1, - // interval: 1000 - // }).pipe(new WriteOrders(this.duckDb)); + new SyncData({ + offset: currentInd, + rpcUrl: this.rpcUrl, + queryTags: [], + limit: 1, + maxThreadLevel: 1, + interval: 1000 + }).pipe(new WriteOrders(this.duckDb, initialData)); } catch (error) { console.log("error in start: ", error); } diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 224a1c29..4c8c7b0e 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -1,3 +1,4 @@ +import { BlockHeader } from "@cosmjs/stargate"; import { Log } from "@cosmjs/stargate/build/logs"; import { Tx } from "@oraichain/cosmos-rpc-sync"; import { Addr, Asset, AssetInfo, Binary, Decimal, SwapOperation, Uint128 } from "@oraichain/oraidex-contracts-sdk"; @@ -36,7 +37,7 @@ export type PairInfoData = { }; export type PriceInfo = { - txheight: string; + txheight: number; timestamp: string; assetInfo: string; price: number; @@ -109,3 +110,13 @@ export type OraiswapPairCw20HookMsg = { export type PairMapping = { asset_infos: [AssetInfo, AssetInfo]; }; + +export type InitialData = { + tokenPrices: Asset[]; + blockHeader: BlockHeader; +}; + +export type PrefixSumHandlingData = { + denom: string; + amount: number; +}; From 384aaa4e8cba4c3659cfbe892c0073a49264d98a Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 09:09:06 +0700 Subject: [PATCH 24/75] created query.ts for other modules to reuse --- packages/oraidex-sync/src/index.ts | 53 +++++------------------ packages/oraidex-sync/src/query.ts | 68 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 42 deletions(-) create mode 100644 packages/oraidex-sync/src/query.ts diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 8d4d753d..e7adc294 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -27,6 +27,7 @@ import { fromBinary, toBinary } from "@cosmjs/cosmwasm-stargate"; import { PoolResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; import { extractUniqueAndFlatten, findAssetInfoPathToUsdt, generateSwapOperations } from "./helper"; import { tenAmountInDecimalSix } from "./constants"; +import { getAllPairInfos, getPoolInfos, simulateSwapPrice } from "./query"; class WriteOrders extends WriteData { private firstWrite: boolean; @@ -115,19 +116,10 @@ class OraiDexSync { this.cosmwasmClient, process.env.MULTICALL_CONTRACT_ADDRES || "orai1q7x644gmf7h8u8y6y8t9z9nnwl8djkmspypr6mxavsk9ual7dj0sxpmgwd" ); - const res = await multicall.tryAggregate({ - queries: pairs.map((pair) => { - return { - address: pair.contract_addr, - data: toBinary({ - pool: {} - }) - }; - }) - }); + const res = await getPoolInfos(pairs, multicall); // reset query client to latest for other functions to call this.cosmwasmClient.setQueryClientWithHeight(); - return res.return_data.map((data) => (data.success ? fromBinary(data.data) : undefined)); + return res; } private async getAllPairInfos(): Promise { @@ -139,48 +131,25 @@ class OraiDexSync { this.cosmwasmClient, process.env.FACTORY_CONTACT_ADDRESS_V2 || "orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" ); - const liquidityResults: PairInfo[] = ( - await Promise.allSettled([ - ...pairs.map((pair) => firstFactoryClient.pair({ assetInfos: pair.asset_infos })), - ...pairs.map((pair) => secondFactoryClient.pair({ assetInfos: pair.asset_infos })) - ]) - ) - .filter((res) => { - if (res.status === "fulfilled") return true; - return false; - }) - .map((data) => (data as any).value as PairInfo); - return liquidityResults; + return getAllPairInfos(firstFactoryClient, secondFactoryClient); } private async simulateSwapPrice(info: AssetInfo, wantedHeight?: number): Promise { // adjust the query height to get data from the past this.cosmwasmClient.setQueryClientWithHeight(wantedHeight); + const routerContract = new OraiswapRouterQueryClient( + this.cosmwasmClient, + process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" + ); const infoPath = findAssetInfoPathToUsdt(info); // usdt case, price is always 1 if (infoPath.length === 1) return { info, amount: tenAmountInDecimalSix.substring(0, tenAmountInDecimalSix.length - 1) }; const operations = generateSwapOperations(info); if (operations.length === 0) return { info, amount: "0" }; // error case. Will be handled by the caller function - const routerContract = new OraiswapRouterQueryClient( - this.cosmwasmClient, - process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" - ); - - try { - const data = await routerContract.simulateSwapOperations({ - offerAmount: tenAmountInDecimalSix, - operations - }); - // reset query client to latest for other functions to call. - this.cosmwasmClient.setQueryClientWithHeight(); - return { info, amount: data.amount.substring(0, data.amount.length - 1) }; // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit - } catch (error) { - console.log(`Error when trying to simulate swap with asset info: ${JSON.stringify(info)} using router: ${error}`); - // reset query client to latest for other functions to call. - this.cosmwasmClient.setQueryClientWithHeight(); - return { info, amount: "0" }; // error case. Will be handled by the caller function - } + const data = await simulateSwapPrice(info, routerContract); + this.cosmwasmClient.setQueryClientWithHeight(); + return data; } public async sync() { diff --git a/packages/oraidex-sync/src/query.ts b/packages/oraidex-sync/src/query.ts new file mode 100644 index 00000000..04819e74 --- /dev/null +++ b/packages/oraidex-sync/src/query.ts @@ -0,0 +1,68 @@ +import { + OraiswapFactoryReadOnlyInterface, + OraiswapRouterReadOnlyInterface, + PairInfo +} from "@oraichain/oraidex-contracts-sdk"; +import { PoolResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; +import { Asset, AssetInfo } from "@oraichain/oraidex-contracts-sdk"; +import { MulticallReadOnlyInterface } from "@oraichain/common-contracts-sdk"; +import { fromBinary, toBinary } from "@cosmjs/cosmwasm-stargate"; +import { pairs } from "./pairs"; +import { findAssetInfoPathToUsdt, generateSwapOperations } from "./helper"; +import { tenAmountInDecimalSix } from "./constants"; + +async function getPoolInfos(pairs: PairInfo[], multicall: MulticallReadOnlyInterface): Promise { + // adjust the query height to get data from the past + const res = await multicall.tryAggregate({ + queries: pairs.map((pair) => { + return { + address: pair.contract_addr, + data: toBinary({ + pool: {} + }) + }; + }) + }); + // reset query client to latest for other functions to call + return res.return_data.map((data) => (data.success ? fromBinary(data.data) : undefined)); +} + +async function getAllPairInfos( + factoryV1: OraiswapFactoryReadOnlyInterface, + factoryV2: OraiswapFactoryReadOnlyInterface +): Promise { + const liquidityResults: PairInfo[] = ( + await Promise.allSettled([ + ...pairs.map((pair) => factoryV1.pair({ assetInfos: pair.asset_infos })), + ...pairs.map((pair) => factoryV2.pair({ assetInfos: pair.asset_infos })) + ]) + ) + .filter((res) => { + if (res.status === "fulfilled") return true; + return false; + }) + .map((data) => (data as any).value as PairInfo); + return liquidityResults; +} + +async function simulateSwapPrice(info: AssetInfo, router: OraiswapRouterReadOnlyInterface): Promise { + // adjust the query height to get data from the past + const infoPath = findAssetInfoPathToUsdt(info); + // usdt case, price is always 1 + if (infoPath.length === 1) + return { info, amount: tenAmountInDecimalSix.substring(0, tenAmountInDecimalSix.length - 1) }; + const operations = generateSwapOperations(info); + if (operations.length === 0) return { info, amount: "0" }; // error case. Will be handled by the caller function + try { + const data = await router.simulateSwapOperations({ + offerAmount: tenAmountInDecimalSix, + operations + }); + return { info, amount: data.amount.substring(0, data.amount.length - 1) }; // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit + } catch (error) { + console.log(`Error when trying to simulate swap with asset info: ${JSON.stringify(info)} using router: ${error}`); + return { info, amount: "0" }; // error case. Will be handled by the caller function + } +} + +export { getAllPairInfos, getPoolInfos, simulateSwapPrice }; From 682c3e2b1e0d418fc8db76b22fe0eb5d1e109266 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 09:27:16 +0700 Subject: [PATCH 25/75] init oraidex server --- packages/oraidex-server/.env.example | 2 + packages/oraidex-server/package.json | 25 ++ packages/oraidex-server/src/index.ts | 27 ++ packages/oraidex-server/tsconfig.json | 15 + packages/oraidex-sync/.env.example | 2 +- packages/oraidex-sync/package.json | 3 +- packages/oraidex-sync/src/index.ts | 5 + yarn.lock | 437 +++++++++++++++++++++++++- 8 files changed, 500 insertions(+), 16 deletions(-) create mode 100644 packages/oraidex-server/.env.example create mode 100644 packages/oraidex-server/package.json create mode 100644 packages/oraidex-server/src/index.ts create mode 100644 packages/oraidex-server/tsconfig.json diff --git a/packages/oraidex-server/.env.example b/packages/oraidex-server/.env.example new file mode 100644 index 00000000..2f790ef3 --- /dev/null +++ b/packages/oraidex-server/.env.example @@ -0,0 +1,2 @@ +PORT=2024 +RPC_URL=https://rpc.orai,io \ No newline at end of file diff --git a/packages/oraidex-server/package.json b/packages/oraidex-server/package.json new file mode 100644 index 00000000..48523cda --- /dev/null +++ b/packages/oraidex-server/package.json @@ -0,0 +1,25 @@ +{ + "name": "@oraichain/oraidex-server", + "version": "1.0.0", + "main": "build/index.js", + "license": "MIT", + "files": [ + "build/" + ], + "scripts": { + "start": "npx ts-node src/index.ts" + }, + "dependencies": { + "@cosmjs/cosmwasm-stargate": "^0.31.0", + "@cosmjs/stargate": "^0.31.0", + "@cosmjs/tendermint-rpc": "^0.31.0", + "@oraichain/cosmos-rpc-sync": "^1.0.5", + "@oraichain/oraidex-sync": "1.0.0", + "cors": "^2.8.5", + "express": "^4.18.2" + }, + "devDependencies": { + "@types/cors": "^2.8.13", + "@types/express": "^4.17.17" + } +} diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts new file mode 100644 index 00000000..da796e40 --- /dev/null +++ b/packages/oraidex-server/src/index.ts @@ -0,0 +1,27 @@ +import * as dotenv from "dotenv"; +import express from "express"; +import { DuckDb, OraiDexSync } from "@oraichain/oraidex-sync"; +import cors from "cors"; + +dotenv.config(); + +const app = express(); +app.use(cors()); + +const port = process.env.PORT || 2024; +let duckDb: DuckDb; + +app.get("/v1/test", async (req, res) => { + const result = await duckDb.conn.all("select count(*) from swap_ops_data;"); + res.status(200).send(result); +}); + +app.listen(port, async () => { + // sync data for the service to read + const duckDb = await DuckDb.create("oraidex-sync-data"); + const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); + await oraidexSync.sync(); + console.log(`[server]: Orderbook Info is running at http://localhost:${port}`); +}); + +// demo pair id in hex form: 5b7b226e61746976655f746f6b656e223a7b2264656e6f6d223a226f726169227d7d2c7b22746f6b656e223a7b22636f6e74726163745f61646472223a226f7261693132687a6a7866683737776c35373267647a637432667876326172786377683667796b63377168227d7d5d diff --git a/packages/oraidex-server/tsconfig.json b/packages/oraidex-server/tsconfig.json new file mode 100644 index 00000000..fa0134db --- /dev/null +++ b/packages/oraidex-server/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "build", + "declaration": true, + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules/" + ] +} \ No newline at end of file diff --git a/packages/oraidex-sync/.env.example b/packages/oraidex-sync/.env.example index ce06d7c9..26e932c4 100644 --- a/packages/oraidex-sync/.env.example +++ b/packages/oraidex-sync/.env.example @@ -1,4 +1,4 @@ -RPC_URL=http://35.237.59.125:26657 +RPC_URL=https://rpc.orai.io FACTORY_CONTACT_ADDRESS_V1="orai1hemdkz4xx9kukgrunxu3yw0nvpyxf34v82d2c8" FACTORY_CONTACT_ADDRESS_V2="orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" ROUTER_CONTRACT_ADDRESS="orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" diff --git a/packages/oraidex-sync/package.json b/packages/oraidex-sync/package.json index 501f401e..c04a8414 100644 --- a/packages/oraidex-sync/package.json +++ b/packages/oraidex-sync/package.json @@ -4,8 +4,7 @@ "main": "build/index.js", "license": "MIT", "files": [ - "build/", - "data/" + "build/" ], "scripts": { "start": "npx ts-node src/index.ts" diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index e7adc294..1f7e1a61 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -215,3 +215,8 @@ const start = async () => { start(); export { OraiDexSync }; + +export * from "./types"; +export * from "./query"; +export * from "./helper"; +export * from "./db"; diff --git a/yarn.lock b/yarn.lock index 189b8bef..7059ff56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2130,6 +2130,48 @@ dependencies: "@babel/types" "^7.20.7" +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.13": + version "2.8.13" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94" + integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.17.35" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f" + integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.17": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/graceful-fs@^4.1.3": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" @@ -2137,6 +2179,11 @@ dependencies: "@types/node" "*" +"@types/http-errors@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65" + integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -2174,6 +2221,16 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + "@types/minimatch@^3.0.3": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -2214,6 +2271,33 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA== +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/send@*": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" + integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.2" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.2.tgz#3e5419ecd1e40e7405d34093f10befb43f63381a" + integrity sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw== + dependencies: + "@types/http-errors" "*" + "@types/mime" "*" + "@types/node" "*" + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" @@ -2264,6 +2348,14 @@ abbrev@1, abbrev@^1.0.0: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" @@ -2405,6 +2497,11 @@ array-differ@^3.0.0: resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" @@ -2584,6 +2681,24 @@ bn.js@^5.2.0: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2665,6 +2780,11 @@ byte-size@^7.0.0: resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-7.0.1.tgz#b1daf3386de7ab9d706b941a748dbfc71130dee3" integrity sha512-crQdqyCwhokxwV1UyDzLZanhkugAgft7vt0qbbdt60C6Zf3CAiGmtUCylbtYwrU6loOUw3euGrNtW1J651ot1A== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + cacache@^16.0.0, cacache@^16.0.6, cacache@^16.1.0: version "16.1.3" resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" @@ -2707,7 +2827,7 @@ cacache@^17.0.0: tar "^6.1.11" unique-filename "^3.0.0" -call-bind@^1.0.2: +call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== @@ -2952,6 +3072,18 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + conventional-changelog-angular@^5.0.12: version "5.0.13" resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c" @@ -3044,11 +3176,29 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + cosmiconfig@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -3112,6 +3262,13 @@ deasync@^0.1.15: bindings "^1.5.0" node-addon-api "^1.7.1" +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -3177,7 +3334,7 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== -depd@^2.0.0: +depd@2.0.0, depd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -3187,6 +3344,11 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" @@ -3282,6 +3444,11 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + ejs@^3.1.7: version "3.1.9" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" @@ -3322,6 +3489,11 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + encoding@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" @@ -3370,6 +3542,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -3385,6 +3562,11 @@ esprima@^4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -3432,6 +3614,43 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== +express@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + external-editor@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" @@ -3508,6 +3727,19 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + find-up@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -3564,6 +3796,16 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -3959,6 +4201,17 @@ http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -3988,7 +4241,7 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -iconv-lite@^0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -4063,7 +4316,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4112,6 +4365,11 @@ ip@^2.0.0: resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + is-arguments@^1.0.4: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" @@ -5079,6 +5337,11 @@ marked@^4.3.0: resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + meow@^8.0.0: version "8.1.2" resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" @@ -5096,6 +5359,11 @@ meow@^8.0.0: type-fest "^0.18.0" yargs-parser "^20.2.3" +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -5106,6 +5374,11 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" @@ -5119,13 +5392,18 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12: +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -5297,12 +5575,17 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.0.0: +ms@2.1.3, ms@^2.0.0: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -5328,7 +5611,7 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -negotiator@^0.6.3: +negotiator@0.6.3, negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== @@ -5623,16 +5906,28 @@ nx@15.9.4, "nx@>=14.8.1 < 16": "@nrwl/nx-win32-arm64-msvc" "15.9.4" "@nrwl/nx-win32-x64-msvc" "15.9.4" -object-assign@^4.1.1: +object-assign@^4, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -5858,6 +6153,11 @@ parse-url@^8.1.0: dependencies: parse-path "^7.0.0" +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + patch-package@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-7.0.0.tgz#5c646b6b4b4bf37e5184a6950777b21dea6bb66e" @@ -5911,6 +6211,11 @@ path-scurry@^1.10.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -6074,6 +6379,14 @@ protocols@^2.0.0, protocols@^2.0.1: resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -6089,6 +6402,13 @@ q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -6099,6 +6419,21 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + react-is@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" @@ -6302,16 +6637,16 @@ rxjs@^7.5.5: dependencies: tslib "^2.1.0" +safe-buffer@5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -6362,11 +6697,45 @@ semver@^7.5.3: dependencies: lru-cache "^6.0.0" +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -6396,6 +6765,15 @@ shiki@^0.14.1: vscode-oniguruma "^1.7.0" vscode-textmate "^8.0.0" +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -6536,6 +6914,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -6757,6 +7140,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -6859,6 +7247,14 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -6929,6 +7325,11 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + upath@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" @@ -6958,6 +7359,11 @@ util@^0.12.4: is-typed-array "^1.1.3" which-typed-array "^1.1.2" +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -7004,6 +7410,11 @@ validate-npm-package-name@^4.0.0: dependencies: builtins "^5.0.0" +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + vscode-oniguruma@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" From 2311bd9458cbf31f4ba1e9f4c8a39c2d1af5297f Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 10:33:49 +0700 Subject: [PATCH 26/75] added price sync and fixed bugs --- packages/oraidex-sync/src/db.ts | 4 +- packages/oraidex-sync/src/helper.ts | 3 +- packages/oraidex-sync/src/index.ts | 36 ++++------ packages/oraidex-sync/src/price-sync.ts | 93 +++++++++++++++++++++++++ packages/oraidex-sync/src/query.ts | 25 ++++++- packages/oraidex-sync/src/types.ts | 12 ++++ 6 files changed, 143 insertions(+), 30 deletions(-) create mode 100644 packages/oraidex-sync/src/price-sync.ts diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 7cc24d1f..4fa64c5c 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -29,9 +29,9 @@ export class DuckDb { await this.conn.exec("CREATE TABLE IF NOT EXISTS height_snapshot (config VARCHAR PRIMARY KEY, value UINTEGER)"); } - async loadHeightSnapshot() { + async loadHeightSnapshot(): Promise { const result = await this.conn.all("SELECT value FROM height_snapshot where config = 'last_block_height'"); - return result.length > 0 ? result[0].value : { currentInd: 1 }; + return result.length > 0 ? (result[0].value as number) : 1; } async insertHeightSnapshot(currentInd: number) { diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index b7432ecc..6f513abc 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -50,8 +50,7 @@ function findAssetInfoPathToUsdt(info: AssetInfo): AssetInfo[] { return [info, ...findAssetInfoPathToUsdt(pairedInfo[0])]; // only need the first found paired token with the one we are matching } -function generateSwapOperations(info: AssetInfo): SwapOperation[] { - const infoPath = findAssetInfoPathToUsdt(info); +function generateSwapOperations(infoPath: AssetInfo[]): SwapOperation[] { let swapOps: SwapOperation[] = []; for (let i = 0; i < infoPath.length - 1; i++) { swapOps.push({ orai_swap: { offer_asset_info: infoPath[i], ask_asset_info: infoPath[i + 1] } } as SwapOperation); diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 1f7e1a61..3f06b7c9 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -2,7 +2,6 @@ import "dotenv/config"; import { parseAssetInfo, parseTxs } from "./tx-parsing"; import { DuckDb } from "./db"; import { WriteData, SyncData, Txs } from "@oraichain/cosmos-rpc-sync"; -import "dotenv/config"; import { pairs } from "./pairs"; import { Asset, @@ -10,11 +9,9 @@ import { CosmWasmClient, OraiswapFactoryQueryClient, OraiswapRouterQueryClient, - PairInfo, - SwapOperation + PairInfo } from "@oraichain/oraidex-contracts-sdk"; import { - PairInfoData, ProvideLiquidityOperationData, SwapOperationData, TxAnlysisResult, @@ -23,11 +20,9 @@ import { PriceInfo } from "./types"; import { MulticallQueryClient } from "@oraichain/common-contracts-sdk"; -import { fromBinary, toBinary } from "@cosmjs/cosmwasm-stargate"; import { PoolResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; -import { extractUniqueAndFlatten, findAssetInfoPathToUsdt, generateSwapOperations } from "./helper"; -import { tenAmountInDecimalSix } from "./constants"; -import { getAllPairInfos, getPoolInfos, simulateSwapPrice } from "./query"; +import { extractUniqueAndFlatten } from "./helper"; +import { getAllPairInfos, getPoolInfos, simulateSwapPriceWithUsdt } from "./query"; class WriteOrders extends WriteData { private firstWrite: boolean; @@ -141,13 +136,7 @@ class OraiDexSync { this.cosmwasmClient, process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" ); - const infoPath = findAssetInfoPathToUsdt(info); - // usdt case, price is always 1 - if (infoPath.length === 1) - return { info, amount: tenAmountInDecimalSix.substring(0, tenAmountInDecimalSix.length - 1) }; - const operations = generateSwapOperations(info); - if (operations.length === 0) return { info, amount: "0" }; // error case. Will be handled by the caller function - const data = await simulateSwapPrice(info, routerContract); + const data = await simulateSwapPriceWithUsdt(info, routerContract); this.cosmwasmClient.setQueryClientWithHeight(); return data; } @@ -158,7 +147,8 @@ class OraiDexSync { this.duckDb.createHeightSnapshot(), this.duckDb.createLiquidityOpsTable(), this.duckDb.createSwapOpsTable(), - this.duckDb.createPairInfosTable() + this.duckDb.createPairInfosTable(), + this.duckDb.createPriceInfoTable() ]); let currentInd = await this.duckDb.loadHeightSnapshot(); let initialData: InitialData = { tokenPrices: [], blockHeader: undefined }; @@ -206,15 +196,15 @@ class OraiDexSync { } } -const start = async () => { - const duckDb = await DuckDb.create("oraidex-sync-data"); - const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); - await oraidexSync.sync(); -}; +// const start = async () => { +// const duckDb = await DuckDb.create("oraidex-sync-data"); +// const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); +// await oraidexSync.sync(); +// }; -start(); +// start(); -export { OraiDexSync }; +// export { OraiDexSync }; export * from "./types"; export * from "./query"; diff --git a/packages/oraidex-sync/src/price-sync.ts b/packages/oraidex-sync/src/price-sync.ts new file mode 100644 index 00000000..c9b9bca6 --- /dev/null +++ b/packages/oraidex-sync/src/price-sync.ts @@ -0,0 +1,93 @@ +import { DuckDb } from "./db"; +import { WriteData, SyncData, Txs } from "@oraichain/cosmos-rpc-sync"; +import { pairs } from "./pairs"; +import { Asset, AssetInfo, CosmWasmClient, OraiswapRouterQueryClient } from "@oraichain/oraidex-contracts-sdk"; +import { PriceInfo } from "./types"; +import { simulateSwapPricePair } from "./query"; +import { parseAssetInfo } from "./tx-parsing"; +import "dotenv/config"; + +class WritePrice extends WriteData { + constructor(private duckDb: DuckDb, private rpcUrl: string) { + super(); + } + + private async simulateSwapPrice(pair: [AssetInfo, AssetInfo], wantedHeight?: number): Promise { + // adjust the query height to get data from the past + const cosmwasmClient = await CosmWasmClient.connect(this.rpcUrl); + cosmwasmClient.setQueryClientWithHeight(wantedHeight); + const routerContract = new OraiswapRouterQueryClient( + cosmwasmClient, + process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" + ); + const data = await simulateSwapPricePair(pair, routerContract); + cosmwasmClient.setQueryClientWithHeight(); + return data; + } + + private async getPrices(): Promise { + let currentInd = await this.duckDb.loadHeightSnapshot(); + console.log("current ind: ", currentInd); + const cosmwasmClient = await CosmWasmClient.connect(this.rpcUrl); + const pairPrices = await Promise.all(pairs.map((pair) => this.simulateSwapPrice(pair.asset_infos, currentInd))); + const blockHeader = (await cosmwasmClient.getBlock(currentInd)).header; + return; + } + + async process(chunk: any): Promise { + try { + const { offset: newOffset } = chunk as Txs; + // console.log("txs: ", txs); + console.log("new offset: ", newOffset); + const queryPrices = await this.getPrices(); + console.table(queryPrices); + // insert txs + await this.duckDb.insertHeightSnapshot(newOffset); + } catch (error) { + console.log("error processing data: ", error); + return false; + } + return true; + } +} + +class PriceSync { + protected constructor(private duckDb: DuckDb, private rpcUrl: string) {} + + public static async create(duckDb: DuckDb, rpcUrl: string): Promise { + return new PriceSync(duckDb, rpcUrl); + } + + public async sync() { + try { + await Promise.all([this.duckDb.createPriceInfoTable(), this.duckDb.createHeightSnapshot()]); + let currentInd = await this.duckDb.loadHeightSnapshot(); + // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic + if (currentInd <= 12388825) { + currentInd = 12388825; + } + await this.duckDb.insertHeightSnapshot(currentInd); + new SyncData({ + offset: currentInd, + rpcUrl: this.rpcUrl, + queryTags: [], + limit: 1000, + maxThreadLevel: 1, + interval: 1000 + }).pipe(new WritePrice(this.duckDb, this.rpcUrl)); + } catch (error) { + console.log("error in start: ", error); + } + } +} + +const start = async () => { + const duckDb = await DuckDb.create("oraidex-sync-data"); + console.log("rpc url: ", process.env.RPC_URL); + const oraidexSync = await PriceSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); + await oraidexSync.sync(); +}; + +start(); + +export { PriceSync }; diff --git a/packages/oraidex-sync/src/query.ts b/packages/oraidex-sync/src/query.ts index 04819e74..21be304b 100644 --- a/packages/oraidex-sync/src/query.ts +++ b/packages/oraidex-sync/src/query.ts @@ -45,13 +45,13 @@ async function getAllPairInfos( return liquidityResults; } -async function simulateSwapPrice(info: AssetInfo, router: OraiswapRouterReadOnlyInterface): Promise { +async function simulateSwapPriceWithUsdt(info: AssetInfo, router: OraiswapRouterReadOnlyInterface): Promise { // adjust the query height to get data from the past const infoPath = findAssetInfoPathToUsdt(info); // usdt case, price is always 1 if (infoPath.length === 1) return { info, amount: tenAmountInDecimalSix.substring(0, tenAmountInDecimalSix.length - 1) }; - const operations = generateSwapOperations(info); + const operations = generateSwapOperations(infoPath); if (operations.length === 0) return { info, amount: "0" }; // error case. Will be handled by the caller function try { const data = await router.simulateSwapOperations({ @@ -65,4 +65,23 @@ async function simulateSwapPrice(info: AssetInfo, router: OraiswapRouterReadOnly } } -export { getAllPairInfos, getPoolInfos, simulateSwapPrice }; +async function simulateSwapPricePair( + pair: [AssetInfo, AssetInfo], + router: OraiswapRouterReadOnlyInterface +): Promise { + // usdt case, price is always 1 + const operations = generateSwapOperations(pair); + if (operations.length === 0) return "0"; // error case. Will be handled by the caller function + try { + const data = await router.simulateSwapOperations({ + offerAmount: tenAmountInDecimalSix, + operations + }); + return data.amount.substring(0, data.amount.length - 1); // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit + } catch (error) { + console.log(`Error when trying to simulate swap with pair: ${JSON.stringify(pair)} using router: ${error}`); + return "0"; // error case. Will be handled by the caller function + } +} + +export { getAllPairInfos, getPoolInfos, simulateSwapPriceWithUsdt, simulateSwapPricePair }; diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 4c8c7b0e..2b86255c 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -120,3 +120,15 @@ export type PrefixSumHandlingData = { denom: string; amount: number; }; + +export type TickerInfo = { + base_currency: string; + target_currency: string; + last_price: string; + base_volume: string; + target_volume: string; + ticker_id: string; + base: string; + target: string; + pool_id: string; +}; From 3801285d1f6b0991707a0ef8662a7ee92dce4bd9 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 13:11:24 +0700 Subject: [PATCH 27/75] finished get pair prices --- packages/oraidex-server/package.json | 1 + packages/oraidex-server/src/index.ts | 132 ++++++++++++++++++++++-- packages/oraidex-sync/src/constants.ts | 1 + packages/oraidex-sync/src/db.ts | 6 ++ packages/oraidex-sync/src/helper.ts | 21 +++- packages/oraidex-sync/src/index.ts | 4 +- packages/oraidex-sync/src/pairs.ts | 33 ++++-- packages/oraidex-sync/src/query.ts | 4 +- packages/oraidex-sync/src/tx-parsing.ts | 26 +---- packages/oraidex-sync/src/types.ts | 1 + 10 files changed, 178 insertions(+), 51 deletions(-) diff --git a/packages/oraidex-server/package.json b/packages/oraidex-server/package.json index 48523cda..f459715a 100644 --- a/packages/oraidex-server/package.json +++ b/packages/oraidex-server/package.json @@ -12,6 +12,7 @@ "dependencies": { "@cosmjs/cosmwasm-stargate": "^0.31.0", "@cosmjs/stargate": "^0.31.0", + "@oraichain/common-contracts-sdk": "1.0.13", "@cosmjs/tendermint-rpc": "^0.31.0", "@oraichain/cosmos-rpc-sync": "^1.0.5", "@oraichain/oraidex-sync": "1.0.0", diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index da796e40..f0334984 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -1,27 +1,137 @@ import * as dotenv from "dotenv"; import express from "express"; -import { DuckDb, OraiDexSync } from "@oraichain/oraidex-sync"; +import { + AssetData, + DuckDb, + OraiDexSync, + PairMapping, + TickerInfo, + pairs, + simulateSwapPricePair, + parseAssetInfoOnlyDenom, + usdtCw20Address, + usdcCw20Address, + getAllPairInfos, + parseAssetInfo, + PairInfoData +} from "@oraichain/oraidex-sync"; import cors from "cors"; +import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { + AssetInfo, + OraiswapFactoryQueryClient, + OraiswapRouterQueryClient, + PairInfo +} from "@oraichain/oraidex-contracts-sdk"; dotenv.config(); const app = express(); app.use(cors()); -const port = process.env.PORT || 2024; let duckDb: DuckDb; -app.get("/v1/test", async (req, res) => { - const result = await duckDb.conn.all("select count(*) from swap_ops_data;"); - res.status(200).send(result); +const port = process.env.PORT || 2024; + +async function queryAllPairInfos(): Promise { + const cosmwasmClient = await CosmWasmClient.connect(process.env.RPC_URL); + const firstFactoryClient = new OraiswapFactoryQueryClient( + cosmwasmClient, + process.env.FACTORY_CONTACT_ADDRESS_V1 || "orai1hemdkz4xx9kukgrunxu3yw0nvpyxf34v82d2c8" + ); + const secondFactoryClient = new OraiswapFactoryQueryClient( + cosmwasmClient, + process.env.FACTORY_CONTACT_ADDRESS_V2 || "orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" + ); + return getAllPairInfos(firstFactoryClient, secondFactoryClient); +} + +app.get("/tickers", async (req, res) => { + const cosmwasmClient = await CosmWasmClient.connect(process.env.RPC_URL); + const routerContract = new OraiswapRouterQueryClient( + cosmwasmClient, + process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" + ); + const pairInfos = await duckDb.queryPairInfos(); + const data: TickerInfo[] = ( + await Promise.allSettled( + pairs.map(async (pair) => { + const symbols = pair.symbols; + const pairAddr = pairInfos.find( + (pairInfo) => + pair.asset_infos.some((info) => parseAssetInfo(info) === pairInfo.firstAssetInfo) && + pair.asset_infos.some((info) => parseAssetInfo(info) === pairInfo.secondAssetInfo) + )?.pairAddr; + try { + const hasUsdInPair = pair.asset_infos.some( + (info) => + parseAssetInfoOnlyDenom(info) === usdtCw20Address || parseAssetInfoOnlyDenom(info) === usdcCw20Address + ); + // reverse because in pairs, we put base info as first index + const price = await simulateSwapPricePair( + hasUsdInPair ? pair.asset_infos : (pair.asset_infos.reverse() as [AssetInfo, AssetInfo]), + routerContract + ); + return { + ticker_id: `${symbols[0]}_${symbols[1]}`, + base_currency: symbols[0], + target_currency: symbols[1], + last_price: price, + base_volume: "0", + target_volume: "0", + pool_id: pairAddr ?? "", + base: symbols[0], + target: symbols[1] + } as TickerInfo; + } catch (error) { + return { + ticker_id: `${symbols[0]}_${symbols[1]}`, + base_currency: symbols[0], + target_currency: symbols[1], + last_price: "0", + base_volume: "0", + target_volume: "0", + pool_id: pairAddr ?? "", + base: symbols[0], + target: symbols[1] + }; + } + }) + ) + ).map((result) => { + if (result.status === "fulfilled") return result.value; + }); + console.table(data); + res.status(200).send("hello world"); }); app.listen(port, async () => { // sync data for the service to read - const duckDb = await DuckDb.create("oraidex-sync-data"); - const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); - await oraidexSync.sync(); - console.log(`[server]: Orderbook Info is running at http://localhost:${port}`); + duckDb = await DuckDb.create("oraidex-sync-data"); + await Promise.all([ + duckDb.createHeightSnapshot(), + duckDb.createLiquidityOpsTable(), + duckDb.createSwapOpsTable(), + duckDb.createPairInfosTable(), + duckDb.createPriceInfoTable() + ]); + const pairInfos = await queryAllPairInfos(); + // Promise.all([insert pool info, and insert pair info. Promise all because pool info & updated pair info must go together]) + await duckDb.insertPairInfos( + pairInfos.map( + (pair) => + ({ + firstAssetInfo: parseAssetInfo(pair.asset_infos[0]), + secondAssetInfo: parseAssetInfo(pair.asset_infos[1]), + commissionRate: pair.commission_rate, + pairAddr: pair.contract_addr, + liquidityAddr: pair.liquidity_token, + oracleAddr: pair.oracle_addr + } as PairInfoData) + ) + ); + // console.dir(pairInfos, { depth: null }); + // const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); + // await oraidexSync.sync(); + console.log(`[server]: oraiDEX info server is running at http://localhost:${port}`); }); - -// demo pair id in hex form: 5b7b226e61746976655f746f6b656e223a7b2264656e6f6d223a226f726169227d7d2c7b22746f6b656e223a7b22636f6e74726163745f61646472223a226f7261693132687a6a7866683737776c35373267647a637432667876326172786377683667796b63377168227d7d5d diff --git a/packages/oraidex-sync/src/constants.ts b/packages/oraidex-sync/src/constants.ts index 5d6a070c..9f31dc59 100644 --- a/packages/oraidex-sync/src/constants.ts +++ b/packages/oraidex-sync/src/constants.ts @@ -11,3 +11,4 @@ export const usdcCw20Address = "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd export const atomIbcDenom = "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"; export const osmosisIbcDenom = "ibc/9C4DCD21B48231D0BC2AC3D1B74A864746B37E4292694C93C617324250D002FC"; export const tenAmountInDecimalSix = "10000000"; +export const priceDecimals = 10 ** 6; diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 4fa64c5c..215b1955 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -99,4 +99,10 @@ export class DuckDb { async queryLpOps() { return this.conn.all("SELECT count(*) from lp_ops_data"); } + + async queryPairInfos(): Promise { + return (await this.conn.all("SELECT firstAssetInfo, secondAssetInfo, pairAddr from pair_infos")).map( + (data) => data as PairInfoData + ); + } } diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index 6f513abc..fe609345 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -1,9 +1,23 @@ import { Asset, AssetInfo, OraiswapRouterReadOnlyInterface, SwapOperation } from "@oraichain/oraidex-contracts-sdk"; -import { parseAssetInfo, parseAssetInfoOnlyDenom } from "./tx-parsing"; import { pairs } from "./pairs"; import { ORAI, usdtCw20Address } from "./constants"; import { PairMapping, PrefixSumHandlingData } from "./types"; +function parseAssetInfo(info: AssetInfo): string { + // if ("native_token" in info) return info.native_token.denom; + // return info.token.contract_addr; + return JSON.stringify(info); +} + +function parseAssetInfoOnlyDenom(info: AssetInfo): string { + if ("native_token" in info) return info.native_token.denom; + return info.token.contract_addr; +} + +async function delay(timeout: number) { + return new Promise((resolve) => setTimeout(resolve, timeout)); +} + function calculatePrefixSum(initialAmount: number, handlingData: PrefixSumHandlingData[]): PrefixSumHandlingData[] { let prefixSumObj = {}; for (let data of handlingData) { @@ -81,5 +95,8 @@ export { findMappedTargetedAssetInfo, findAssetInfoPathToUsdt, generateSwapOperations, - extractUniqueAndFlatten + extractUniqueAndFlatten, + parseAssetInfo, + parseAssetInfoOnlyDenom, + delay }; diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 3f06b7c9..b9ceafb6 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -204,9 +204,11 @@ class OraiDexSync { // start(); -// export { OraiDexSync }; +export { OraiDexSync }; export * from "./types"; export * from "./query"; export * from "./helper"; export * from "./db"; +export * from "./pairs"; +export * from "./constants"; diff --git a/packages/oraidex-sync/src/pairs.ts b/packages/oraidex-sync/src/pairs.ts index a7810f3b..e6ae3844 100644 --- a/packages/oraidex-sync/src/pairs.ts +++ b/packages/oraidex-sync/src/pairs.ts @@ -18,22 +18,28 @@ import { PairMapping } from "./types"; export const pairs: PairMapping[] = [ { - asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: airiCw20Adress } }] + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: airiCw20Adress } }], + symbols: ["ORAI", "AIRI"] }, { - asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: oraixCw20Address } }] + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: oraixCw20Address } }], + symbols: ["ORAI", "ORAIX"] }, { - asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: scOraiCw20Address } }] + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: scOraiCw20Address } }], + symbols: ["ORAI", "scORAI"] }, { - asset_infos: [{ native_token: { denom: ORAI } }, { native_token: { denom: atomIbcDenom } }] + asset_infos: [{ native_token: { denom: ORAI } }, { native_token: { denom: atomIbcDenom } }], + symbols: ["ORAI", "ATOM"] }, { - asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdtCw20Address } }] + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdtCw20Address } }], + symbols: ["ORAI", "USDT"] }, { - asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: kwtCw20Address } }] + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: kwtCw20Address } }], + symbols: ["ORAI", "KWT"] }, { asset_infos: [ @@ -41,18 +47,23 @@ export const pairs: PairMapping[] = [ { native_token: { denom: osmosisIbcDenom } } - ] + ], + symbols: ["ORAI", "OSMOSIS"] }, { - asset_infos: [{ token: { contract_addr: milkyCw20Address } }, { token: { contract_addr: usdtCw20Address } }] + asset_infos: [{ token: { contract_addr: milkyCw20Address } }, { token: { contract_addr: usdtCw20Address } }], + symbols: ["MILKY", "USDT"] }, { - asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdcCw20Address } }] + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdcCw20Address } }], + symbols: ["ORAI", "USDC"] }, { - asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: tronCw20Address } }] + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: tronCw20Address } }], + symbols: ["ORAI", "WTRX"] }, { - asset_infos: [{ native_token: { denom: atomIbcDenom } }, { token: { contract_addr: scAtomCw20Address } }] + asset_infos: [{ native_token: { denom: atomIbcDenom } }, { token: { contract_addr: scAtomCw20Address } }], + symbols: ["ATOM", "scATOM"] } ]; diff --git a/packages/oraidex-sync/src/query.ts b/packages/oraidex-sync/src/query.ts index 21be304b..667ae09f 100644 --- a/packages/oraidex-sync/src/query.ts +++ b/packages/oraidex-sync/src/query.ts @@ -9,7 +9,7 @@ import { MulticallReadOnlyInterface } from "@oraichain/common-contracts-sdk"; import { fromBinary, toBinary } from "@cosmjs/cosmwasm-stargate"; import { pairs } from "./pairs"; import { findAssetInfoPathToUsdt, generateSwapOperations } from "./helper"; -import { tenAmountInDecimalSix } from "./constants"; +import { priceDecimals, tenAmountInDecimalSix } from "./constants"; async function getPoolInfos(pairs: PairInfo[], multicall: MulticallReadOnlyInterface): Promise { // adjust the query height to get data from the past @@ -77,7 +77,7 @@ async function simulateSwapPricePair( offerAmount: tenAmountInDecimalSix, operations }); - return data.amount.substring(0, data.amount.length - 1); // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit + return (parseInt(data.amount) / priceDecimals / 10).toString(); // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit } catch (error) { console.log(`Error when trying to simulate swap with pair: ${JSON.stringify(pair)} using router: ${error}`); return "0"; // error case. Will be handled by the caller function diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index d37f7491..b4093ed2 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -16,21 +16,7 @@ import { WithdrawLiquidityOperationData } from "./types"; import { Log } from "@cosmjs/stargate/build/logs"; - -function parseAssetInfo(info: AssetInfo): string { - // if ("native_token" in info) return info.native_token.denom; - // return info.token.contract_addr; - return JSON.stringify(info); -} - -function parseAssetInfoOnlyDenom(info: AssetInfo): string { - if ("native_token" in info) return info.native_token.denom; - return info.token.contract_addr; -} - -async function delay(timeout: number) { - return new Promise((resolve) => setTimeout(resolve, timeout)); -} +import { parseAssetInfo } from "./helper"; function parseWasmEvents(events: readonly Event[]): (readonly Attribute[])[] { return events.filter((event) => event.type === "wasm").map((event) => event.attributes); @@ -231,12 +217,4 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { }; } -export { - parseAssetInfo, - delay, - parseWasmEvents, - parseTxs, - parseWithdrawLiquidityAssets, - parseTxToMsgExecuteContractMsgs, - parseAssetInfoOnlyDenom -}; +export { parseAssetInfo, parseWasmEvents, parseTxs, parseWithdrawLiquidityAssets, parseTxToMsgExecuteContractMsgs }; diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 2b86255c..dbe69960 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -109,6 +109,7 @@ export type OraiswapPairCw20HookMsg = { }; export type PairMapping = { asset_infos: [AssetInfo, AssetInfo]; + symbols: [string, string]; }; export type InitialData = { From 3c603e4f8e2f28774f08671ba278e8e00a8ea783 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 13:44:15 +0700 Subject: [PATCH 28/75] added endpoint /pairs for coingecko --- packages/oraidex-server/src/index.ts | 28 ++++++++++++++++++++++------ packages/oraidex-sync/src/helper.ts | 12 ++++++++++-- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index f0334984..4225acbb 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -13,7 +13,8 @@ import { usdcCw20Address, getAllPairInfos, parseAssetInfo, - PairInfoData + PairInfoData, + findPairAddress } from "@oraichain/oraidex-sync"; import cors from "cors"; import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; @@ -46,6 +47,25 @@ async function queryAllPairInfos(): Promise { return getAllPairInfos(firstFactoryClient, secondFactoryClient); } +app.get("/pairs", async (req, res) => { + try { + const pairInfos = await duckDb.queryPairInfos(); + res.status(200).send( + pairs.map((pair) => { + const pairAddr = findPairAddress(pairInfos, pair.asset_infos); + return { + ticker_id: `${pair.symbols[0]}_${pair.symbols[1]}`, + base: pair.symbols[0], + target: pair.symbols[1], + pool_id: pairAddr ?? "" + }; + }) + ); + } catch (error) { + res.status(500).send(`Error getting pair infos: ${JSON.stringify(error)}`); + } +}); + app.get("/tickers", async (req, res) => { const cosmwasmClient = await CosmWasmClient.connect(process.env.RPC_URL); const routerContract = new OraiswapRouterQueryClient( @@ -57,11 +77,7 @@ app.get("/tickers", async (req, res) => { await Promise.allSettled( pairs.map(async (pair) => { const symbols = pair.symbols; - const pairAddr = pairInfos.find( - (pairInfo) => - pair.asset_infos.some((info) => parseAssetInfo(info) === pairInfo.firstAssetInfo) && - pair.asset_infos.some((info) => parseAssetInfo(info) === pairInfo.secondAssetInfo) - )?.pairAddr; + const pairAddr = findPairAddress(pairInfos, pair.asset_infos); try { const hasUsdInPair = pair.asset_infos.some( (info) => diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index fe609345..7533cb61 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -1,7 +1,7 @@ import { Asset, AssetInfo, OraiswapRouterReadOnlyInterface, SwapOperation } from "@oraichain/oraidex-contracts-sdk"; import { pairs } from "./pairs"; import { ORAI, usdtCw20Address } from "./constants"; -import { PairMapping, PrefixSumHandlingData } from "./types"; +import { PairInfoData, PairMapping, PrefixSumHandlingData } from "./types"; function parseAssetInfo(info: AssetInfo): string { // if ("native_token" in info) return info.native_token.denom; @@ -89,6 +89,13 @@ function extractUniqueAndFlatten(data: PairMapping[]): AssetInfo[] { return uniqueFlattenedArray; } +function findPairAddress(pairInfos: PairInfoData[], infos: [AssetInfo, AssetInfo]) { + return pairInfos.find( + (pairInfo) => + infos.some((info) => parseAssetInfo(info) === pairInfo.firstAssetInfo) && + infos.some((info) => parseAssetInfo(info) === pairInfo.secondAssetInfo) + )?.pairAddr; +} export { calculatePrefixSum, @@ -98,5 +105,6 @@ export { extractUniqueAndFlatten, parseAssetInfo, parseAssetInfoOnlyDenom, - delay + delay, + findPairAddress }; From b1ee494e2cdc659dd7d42a8c510240b1b7351f89 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 13:45:43 +0700 Subject: [PATCH 29/75] added hostname to expose server to public --- packages/oraidex-server/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 4225acbb..b648b88f 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -32,7 +32,7 @@ app.use(cors()); let duckDb: DuckDb; -const port = process.env.PORT || 2024; +const port = parseInt(process.env.PORT) || 2024; async function queryAllPairInfos(): Promise { const cosmwasmClient = await CosmWasmClient.connect(process.env.RPC_URL); @@ -121,7 +121,7 @@ app.get("/tickers", async (req, res) => { res.status(200).send("hello world"); }); -app.listen(port, async () => { +app.listen(port, "0.0.0.0", async () => { // sync data for the service to read duckDb = await DuckDb.create("oraidex-sync-data"); await Promise.all([ From c60e5ab24144d781772b6dce31523aff77c0fd16 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 13:49:52 +0700 Subject: [PATCH 30/75] added start server script & fallback rpc url --- package.json | 4 +++- packages/oraidex-server/src/index.ts | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e61005af..d386fdb8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "test": "jest", "docs": "typedoc --entryPointStrategy expand --name 'Oraidex SDK' --readme none --tsconfig packages/contracts-sdk/tsconfig.json packages/contracts-sdk/src", "build": "tsc -p", - "deploy": "yarn publish --access public" + "deploy": "yarn publish --access public", + "start:server": "npx ts-node packages/oraidex-server/src/index.ts" + }, "workspaces": [ "packages/*" diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index b648b88f..78f26bd7 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -33,9 +33,10 @@ app.use(cors()); let duckDb: DuckDb; const port = parseInt(process.env.PORT) || 2024; +const rpcUrl = process.env.RPC_URL || "https://rpc.orai.io"; async function queryAllPairInfos(): Promise { - const cosmwasmClient = await CosmWasmClient.connect(process.env.RPC_URL); + const cosmwasmClient = await CosmWasmClient.connect(rpcUrl); const firstFactoryClient = new OraiswapFactoryQueryClient( cosmwasmClient, process.env.FACTORY_CONTACT_ADDRESS_V1 || "orai1hemdkz4xx9kukgrunxu3yw0nvpyxf34v82d2c8" @@ -67,7 +68,7 @@ app.get("/pairs", async (req, res) => { }); app.get("/tickers", async (req, res) => { - const cosmwasmClient = await CosmWasmClient.connect(process.env.RPC_URL); + const cosmwasmClient = await CosmWasmClient.connect(rpcUrl); const routerContract = new OraiswapRouterQueryClient( cosmwasmClient, process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" From 3995e3e077999b49a7dfe7e9aab214ecee7ecc1d Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 13:57:30 +0700 Subject: [PATCH 31/75] added hostname --- packages/oraidex-server/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 78f26bd7..c5626fbf 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -33,6 +33,7 @@ app.use(cors()); let duckDb: DuckDb; const port = parseInt(process.env.PORT) || 2024; +const hostname = process.env.HOSTNAME || "0.0.0.0"; const rpcUrl = process.env.RPC_URL || "https://rpc.orai.io"; async function queryAllPairInfos(): Promise { @@ -122,7 +123,7 @@ app.get("/tickers", async (req, res) => { res.status(200).send("hello world"); }); -app.listen(port, "0.0.0.0", async () => { +app.listen(port, hostname, async () => { // sync data for the service to read duckDb = await DuckDb.create("oraidex-sync-data"); await Promise.all([ @@ -150,5 +151,5 @@ app.listen(port, "0.0.0.0", async () => { // console.dir(pairInfos, { depth: null }); // const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); // await oraidexSync.sync(); - console.log(`[server]: oraiDEX info server is running at http://localhost:${port}`); + console.log(`[server]: oraiDEX info server is running at http://${hostname}:${port}`); }); From a586a81b6aa44ba1c56282994323917318a20dbe Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 13:58:43 +0700 Subject: [PATCH 32/75] tickers returned data instead of hello world --- packages/oraidex-server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index c5626fbf..77ecf4d1 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -120,7 +120,7 @@ app.get("/tickers", async (req, res) => { if (result.status === "fulfilled") return result.value; }); console.table(data); - res.status(200).send("hello world"); + res.status(200).send(data); }); app.listen(port, hostname, async () => { From f32d59c790f6a4f382e83995f52d2f1fafced223 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 13:59:37 +0700 Subject: [PATCH 33/75] added error handling /tickers api --- packages/oraidex-server/src/index.ts | 108 ++++++++++++++------------- 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 77ecf4d1..4c312f3f 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -69,58 +69,62 @@ app.get("/pairs", async (req, res) => { }); app.get("/tickers", async (req, res) => { - const cosmwasmClient = await CosmWasmClient.connect(rpcUrl); - const routerContract = new OraiswapRouterQueryClient( - cosmwasmClient, - process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" - ); - const pairInfos = await duckDb.queryPairInfos(); - const data: TickerInfo[] = ( - await Promise.allSettled( - pairs.map(async (pair) => { - const symbols = pair.symbols; - const pairAddr = findPairAddress(pairInfos, pair.asset_infos); - try { - const hasUsdInPair = pair.asset_infos.some( - (info) => - parseAssetInfoOnlyDenom(info) === usdtCw20Address || parseAssetInfoOnlyDenom(info) === usdcCw20Address - ); - // reverse because in pairs, we put base info as first index - const price = await simulateSwapPricePair( - hasUsdInPair ? pair.asset_infos : (pair.asset_infos.reverse() as [AssetInfo, AssetInfo]), - routerContract - ); - return { - ticker_id: `${symbols[0]}_${symbols[1]}`, - base_currency: symbols[0], - target_currency: symbols[1], - last_price: price, - base_volume: "0", - target_volume: "0", - pool_id: pairAddr ?? "", - base: symbols[0], - target: symbols[1] - } as TickerInfo; - } catch (error) { - return { - ticker_id: `${symbols[0]}_${symbols[1]}`, - base_currency: symbols[0], - target_currency: symbols[1], - last_price: "0", - base_volume: "0", - target_volume: "0", - pool_id: pairAddr ?? "", - base: symbols[0], - target: symbols[1] - }; - } - }) - ) - ).map((result) => { - if (result.status === "fulfilled") return result.value; - }); - console.table(data); - res.status(200).send(data); + try { + const cosmwasmClient = await CosmWasmClient.connect(rpcUrl); + const routerContract = new OraiswapRouterQueryClient( + cosmwasmClient, + process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" + ); + const pairInfos = await duckDb.queryPairInfos(); + const data: TickerInfo[] = ( + await Promise.allSettled( + pairs.map(async (pair) => { + const symbols = pair.symbols; + const pairAddr = findPairAddress(pairInfos, pair.asset_infos); + try { + const hasUsdInPair = pair.asset_infos.some( + (info) => + parseAssetInfoOnlyDenom(info) === usdtCw20Address || parseAssetInfoOnlyDenom(info) === usdcCw20Address + ); + // reverse because in pairs, we put base info as first index + const price = await simulateSwapPricePair( + hasUsdInPair ? pair.asset_infos : (pair.asset_infos.reverse() as [AssetInfo, AssetInfo]), + routerContract + ); + return { + ticker_id: `${symbols[0]}_${symbols[1]}`, + base_currency: symbols[0], + target_currency: symbols[1], + last_price: price, + base_volume: "0", + target_volume: "0", + pool_id: pairAddr ?? "", + base: symbols[0], + target: symbols[1] + } as TickerInfo; + } catch (error) { + return { + ticker_id: `${symbols[0]}_${symbols[1]}`, + base_currency: symbols[0], + target_currency: symbols[1], + last_price: "0", + base_volume: "0", + target_volume: "0", + pool_id: pairAddr ?? "", + base: symbols[0], + target: symbols[1] + }; + } + }) + ) + ).map((result) => { + if (result.status === "fulfilled") return result.value; + }); + console.table(data); + res.status(200).send(data); + } catch (error) { + res.status(500).send(`Error: ${JSON.stringify(error)}`); + } }); app.listen(port, hostname, async () => { From 1a6c871315f941ba5489f2338ec5fda07862919f Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 14:15:59 +0700 Subject: [PATCH 34/75] added test findPairAddress --- .gitignore | 3 +-- packages/oraidex-sync/tests/helper.spec.ts | 30 ++++++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 57902e3c..908329c0 100644 --- a/.gitignore +++ b/.gitignore @@ -27,5 +27,4 @@ typings/ .npmrc build -*data -*data.wal \ No newline at end of file +*oraidex-sync-data* \ No newline at end of file diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index 4196664a..e7d77dae 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -1,18 +1,21 @@ -import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; +import { Asset, AssetInfo } from "@oraichain/oraidex-contracts-sdk"; import { calculatePrefixSum, findAssetInfoPathToUsdt, findMappedTargetedAssetInfo, - extractUniqueAndFlatten + extractUniqueAndFlatten, + findPairAddress } from "../src/helper"; import { pairs } from "../src/pairs"; import { + ORAI, airiCw20Adress, milkyCw20Address, scAtomCw20Address, usdcCw20Address, usdtCw20Address } from "../src/constants"; +import { PairInfoData } from "../src/types"; describe("test-helper", () => { it.each<[AssetInfo, number]>([ @@ -98,4 +101,27 @@ describe("test-helper", () => { } ]); }); + it.each<[AssetInfo, string | undefined]>([ + [{ token: { contract_addr: usdtCw20Address } }, "orai1c5s03c3l336dgesne7dylnmhszw8554tsyy9yt"], + [{ token: { contract_addr: "foo" } }, undefined] + ])("test-findPairAddress", (assetInfo, expectedPairAddr) => { + // setup + let pairInfoData: PairInfoData[] = [ + { + firstAssetInfo: JSON.stringify({ native_token: { denom: ORAI } } as AssetInfo), + secondAssetInfo: JSON.stringify({ token: { contract_addr: usdtCw20Address } } as AssetInfo), + commissionRate: "", + pairAddr: "orai1c5s03c3l336dgesne7dylnmhszw8554tsyy9yt", + liquidityAddr: "", + oracleAddr: "" + } + ]; + let assetInfos: [AssetInfo, AssetInfo] = [{ native_token: { denom: ORAI } }, assetInfo]; + + // act + const result = findPairAddress(pairInfoData, assetInfos); + + // assert + expect(result).toEqual(expectedPairAddr); + }); }); From 7b7c415691df539645f6b45b014803a2a15f1e78 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 14:33:08 +0700 Subject: [PATCH 35/75] added parsed symbol to ticker function & reverse ticker order --- packages/oraidex-server/src/helper.ts | 3 +++ packages/oraidex-server/src/index.ts | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 packages/oraidex-server/src/helper.ts diff --git a/packages/oraidex-server/src/helper.ts b/packages/oraidex-server/src/helper.ts new file mode 100644 index 00000000..40304e66 --- /dev/null +++ b/packages/oraidex-server/src/helper.ts @@ -0,0 +1,3 @@ +export function parseSymbolsToTickerId(symbols: [string, string]) { + return `${symbols[1]}_${symbols[0]}`; +} diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 4c312f3f..4ba33920 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -24,6 +24,7 @@ import { OraiswapRouterQueryClient, PairInfo } from "@oraichain/oraidex-contracts-sdk"; +import { parseSymbolsToTickerId } from "./helper"; dotenv.config(); @@ -56,7 +57,7 @@ app.get("/pairs", async (req, res) => { pairs.map((pair) => { const pairAddr = findPairAddress(pairInfos, pair.asset_infos); return { - ticker_id: `${pair.symbols[0]}_${pair.symbols[1]}`, + ticker_id: parseSymbolsToTickerId(pair.symbols), base: pair.symbols[0], target: pair.symbols[1], pool_id: pairAddr ?? "" @@ -92,7 +93,7 @@ app.get("/tickers", async (req, res) => { routerContract ); return { - ticker_id: `${symbols[0]}_${symbols[1]}`, + ticker_id: parseSymbolsToTickerId(symbols), base_currency: symbols[0], target_currency: symbols[1], last_price: price, From 5bd20449b744509cacbc8e273dabf5f0514eb080 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 14:41:26 +0700 Subject: [PATCH 36/75] fixed wrong base & target currency --- packages/oraidex-server/src/helper.ts | 2 +- packages/oraidex-server/src/index.ts | 33 +++++++++++++++------------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/oraidex-server/src/helper.ts b/packages/oraidex-server/src/helper.ts index 40304e66..3bb568ef 100644 --- a/packages/oraidex-server/src/helper.ts +++ b/packages/oraidex-server/src/helper.ts @@ -1,3 +1,3 @@ export function parseSymbolsToTickerId(symbols: [string, string]) { - return `${symbols[1]}_${symbols[0]}`; + return `${symbols[0]}_${symbols[1]}`; } diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 4ba33920..3f8dd812 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -82,38 +82,41 @@ app.get("/tickers", async (req, res) => { pairs.map(async (pair) => { const symbols = pair.symbols; const pairAddr = findPairAddress(pairInfos, pair.asset_infos); + const tickerId = parseSymbolsToTickerId(symbols); + const hasUsdInPair = pair.asset_infos.some( + (info) => + parseAssetInfoOnlyDenom(info) === usdtCw20Address || parseAssetInfoOnlyDenom(info) === usdcCw20Address + ); + const currencies = hasUsdInPair ? pair.symbols.reverse() : pair.symbols; + const assetInfosForSimulation = hasUsdInPair ? pair.asset_infos : pair.asset_infos.reverse(); try { - const hasUsdInPair = pair.asset_infos.some( - (info) => - parseAssetInfoOnlyDenom(info) === usdtCw20Address || parseAssetInfoOnlyDenom(info) === usdcCw20Address - ); // reverse because in pairs, we put base info as first index const price = await simulateSwapPricePair( - hasUsdInPair ? pair.asset_infos : (pair.asset_infos.reverse() as [AssetInfo, AssetInfo]), + assetInfosForSimulation as [AssetInfo, AssetInfo], routerContract ); return { - ticker_id: parseSymbolsToTickerId(symbols), - base_currency: symbols[0], - target_currency: symbols[1], + ticker_id: tickerId, + base_currency: currencies[0], + target_currency: currencies[1], last_price: price, base_volume: "0", target_volume: "0", pool_id: pairAddr ?? "", - base: symbols[0], - target: symbols[1] + base: currencies[0], + target: currencies[1] } as TickerInfo; } catch (error) { return { - ticker_id: `${symbols[0]}_${symbols[1]}`, - base_currency: symbols[0], - target_currency: symbols[1], + ticker_id: tickerId, + base_currency: currencies[0], + target_currency: currencies[1], last_price: "0", base_volume: "0", target_volume: "0", pool_id: pairAddr ?? "", - base: symbols[0], - target: symbols[1] + base: currencies[0], + target: currencies[1] }; } }) From 7d92b1468e50d642d01d6703cccf6e6f92095da4 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 14:42:38 +0700 Subject: [PATCH 37/75] removed price-sync --- packages/oraidex-sync/src/price-sync.ts | 93 ------------------------- 1 file changed, 93 deletions(-) delete mode 100644 packages/oraidex-sync/src/price-sync.ts diff --git a/packages/oraidex-sync/src/price-sync.ts b/packages/oraidex-sync/src/price-sync.ts deleted file mode 100644 index c9b9bca6..00000000 --- a/packages/oraidex-sync/src/price-sync.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { DuckDb } from "./db"; -import { WriteData, SyncData, Txs } from "@oraichain/cosmos-rpc-sync"; -import { pairs } from "./pairs"; -import { Asset, AssetInfo, CosmWasmClient, OraiswapRouterQueryClient } from "@oraichain/oraidex-contracts-sdk"; -import { PriceInfo } from "./types"; -import { simulateSwapPricePair } from "./query"; -import { parseAssetInfo } from "./tx-parsing"; -import "dotenv/config"; - -class WritePrice extends WriteData { - constructor(private duckDb: DuckDb, private rpcUrl: string) { - super(); - } - - private async simulateSwapPrice(pair: [AssetInfo, AssetInfo], wantedHeight?: number): Promise { - // adjust the query height to get data from the past - const cosmwasmClient = await CosmWasmClient.connect(this.rpcUrl); - cosmwasmClient.setQueryClientWithHeight(wantedHeight); - const routerContract = new OraiswapRouterQueryClient( - cosmwasmClient, - process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" - ); - const data = await simulateSwapPricePair(pair, routerContract); - cosmwasmClient.setQueryClientWithHeight(); - return data; - } - - private async getPrices(): Promise { - let currentInd = await this.duckDb.loadHeightSnapshot(); - console.log("current ind: ", currentInd); - const cosmwasmClient = await CosmWasmClient.connect(this.rpcUrl); - const pairPrices = await Promise.all(pairs.map((pair) => this.simulateSwapPrice(pair.asset_infos, currentInd))); - const blockHeader = (await cosmwasmClient.getBlock(currentInd)).header; - return; - } - - async process(chunk: any): Promise { - try { - const { offset: newOffset } = chunk as Txs; - // console.log("txs: ", txs); - console.log("new offset: ", newOffset); - const queryPrices = await this.getPrices(); - console.table(queryPrices); - // insert txs - await this.duckDb.insertHeightSnapshot(newOffset); - } catch (error) { - console.log("error processing data: ", error); - return false; - } - return true; - } -} - -class PriceSync { - protected constructor(private duckDb: DuckDb, private rpcUrl: string) {} - - public static async create(duckDb: DuckDb, rpcUrl: string): Promise { - return new PriceSync(duckDb, rpcUrl); - } - - public async sync() { - try { - await Promise.all([this.duckDb.createPriceInfoTable(), this.duckDb.createHeightSnapshot()]); - let currentInd = await this.duckDb.loadHeightSnapshot(); - // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic - if (currentInd <= 12388825) { - currentInd = 12388825; - } - await this.duckDb.insertHeightSnapshot(currentInd); - new SyncData({ - offset: currentInd, - rpcUrl: this.rpcUrl, - queryTags: [], - limit: 1000, - maxThreadLevel: 1, - interval: 1000 - }).pipe(new WritePrice(this.duckDb, this.rpcUrl)); - } catch (error) { - console.log("error in start: ", error); - } - } -} - -const start = async () => { - const duckDb = await DuckDb.create("oraidex-sync-data"); - console.log("rpc url: ", process.env.RPC_URL); - const oraidexSync = await PriceSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); - await oraidexSync.sync(); -}; - -start(); - -export { PriceSync }; From 8a1dcd471a403863963d508dc9c1ad997d8a48b9 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 14:49:42 +0700 Subject: [PATCH 38/75] refactored updated latest pair infos --- packages/oraidex-sync/src/index.ts | 39 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index b9ceafb6..2380233c 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -17,7 +17,8 @@ import { TxAnlysisResult, WithdrawLiquidityOperationData, InitialData, - PriceInfo + PriceInfo, + PairInfoData } from "./types"; import { MulticallQueryClient } from "@oraichain/common-contracts-sdk"; import { PoolResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; @@ -141,6 +142,23 @@ class OraiDexSync { return data; } + private async updateLatestPairInfos() { + const pairInfos = await this.getAllPairInfos(); + await this.duckDb.insertPairInfos( + pairInfos.map( + (pair) => + ({ + firstAssetInfo: parseAssetInfo(pair.asset_infos[0]), + secondAssetInfo: parseAssetInfo(pair.asset_infos[1]), + commissionRate: pair.commission_rate, + pairAddr: pair.contract_addr, + liquidityAddr: pair.liquidity_token, + oracleAddr: pair.oracle_addr + } as PairInfoData) + ) + ); + } + public async sync() { try { await Promise.all([ @@ -164,24 +182,7 @@ class OraiDexSync { const initialBlockHeader = (await this.cosmwasmClient.getBlock(currentInd)).header; initialData.tokenPrices = tokenPrices; initialData.blockHeader = initialBlockHeader; - // const pairInfos = await this.getAllPairInfos(); - // // TODO: only get pool infos of selected pairs if that pair does not exist in the pair info database, meaning it is new. Otherwise, it would have been called before and stored the pool result given the wanted height. - // const poolResultsAtOldHeight = await this.getPoolInfos(pairInfos, currentInd); - // // Promise.all([insert pool info, and insert pair info. Promise all because pool info & updated pair info must go together]) - // await this.duckDb.insertPairInfos( - // pairInfos.map( - // (pair) => - // ({ - // firstAssetInfo: parseAssetInfo(pair.asset_infos[0]), - // secondAssetInfo: parseAssetInfo(pair.asset_infos[1]), - // commissionRate: pair.commission_rate, - // pairAddr: pair.contract_addr, - // liquidityAddr: pair.liquidity_token, - // oracleAddr: pair.oracle_addr - // } as PairInfoData) - // ) - // ); - // // console.dir(pairInfos, { depth: null }); + await this.updateLatestPairInfos(); new SyncData({ offset: currentInd, rpcUrl: this.rpcUrl, From 87c6b8c681c11fcca041eafeb791e15dec39a775 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 15:12:41 +0700 Subject: [PATCH 39/75] added volume & total lp for swap & lp tables & added calculatePriceByPool --- packages/oraidex-sync/src/constants.ts | 2 +- packages/oraidex-sync/src/db.ts | 4 ++-- packages/oraidex-sync/src/helper.ts | 9 +++++++-- packages/oraidex-sync/src/query.ts | 2 +- packages/oraidex-sync/tests/helper.spec.ts | 8 +++++++- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/oraidex-sync/src/constants.ts b/packages/oraidex-sync/src/constants.ts index 9f31dc59..cdfa22d8 100644 --- a/packages/oraidex-sync/src/constants.ts +++ b/packages/oraidex-sync/src/constants.ts @@ -11,4 +11,4 @@ export const usdcCw20Address = "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd export const atomIbcDenom = "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"; export const osmosisIbcDenom = "ibc/9C4DCD21B48231D0BC2AC3D1B74A864746B37E4292694C93C617324250D002FC"; export const tenAmountInDecimalSix = "10000000"; -export const priceDecimals = 10 ** 6; +export const priceDecimals = 10 ** 7; diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 215b1955..3eca9b9b 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -41,7 +41,7 @@ export class DuckDb { // swap operation table handling async createSwapOpsTable() { await this.conn.exec( - "CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, timestamp TIMESTAMP, offerDenom VARCHAR, offerAmount UBIGINT, askDenom VARCHAR, returnAmount UBIGINT, taxAmount UBIGINT, commissionAmount UBIGINT, spreadAmount UBIGINT)" + "CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, timestamp TIMESTAMP, offerDenom VARCHAR, offerAmount UBIGINT, offerVolume UBIGINT, askDenom VARCHAR, askVolume UBIGINT, returnAmount UBIGINT, taxAmount UBIGINT, commissionAmount UBIGINT, spreadAmount UBIGINT)" ); } @@ -58,7 +58,7 @@ export class DuckDb { await this.conn.exec("CREATE TYPE LPOPTYPE AS ENUM ('provide','withdraw');"); } await this.conn.exec( - "CREATE TABLE IF NOT EXISTS lp_ops_data (txhash VARCHAR, timestamp TIMESTAMP, firstTokenAmount UBIGINT, firstTokenDenom VARCHAR, secondTokenAmount UBIGINT, secondTokenDenom VARCHAR, txCreator VARCHAR, opType LPOPTYPE)" + "CREATE TABLE IF NOT EXISTS lp_ops_data (txhash VARCHAR, timestamp TIMESTAMP, firstTokenAmount UBIGINT, firstTokenLp UBIGINT, firstTokenDenom VARCHAR, secondTokenAmount UBIGINT, secondTokenLp UBIGINT, secondTokenDenom VARCHAR, txCreator VARCHAR, opType LPOPTYPE)" ); } diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index 7533cb61..edb15f43 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -1,6 +1,6 @@ import { Asset, AssetInfo, OraiswapRouterReadOnlyInterface, SwapOperation } from "@oraichain/oraidex-contracts-sdk"; import { pairs } from "./pairs"; -import { ORAI, usdtCw20Address } from "./constants"; +import { ORAI, tenAmountInDecimalSix, usdtCw20Address } from "./constants"; import { PairInfoData, PairMapping, PrefixSumHandlingData } from "./types"; function parseAssetInfo(info: AssetInfo): string { @@ -97,6 +97,10 @@ function findPairAddress(pairInfos: PairInfoData[], infos: [AssetInfo, AssetInfo )?.pairAddr; } +function calculatePriceByPool(offerPool: bigint, askPool: bigint, commissionRate: number): bigint { + return (askPool - (offerPool * askPool) / (offerPool + BigInt(tenAmountInDecimalSix))) * BigInt(1 - commissionRate); +} + export { calculatePrefixSum, findMappedTargetedAssetInfo, @@ -106,5 +110,6 @@ export { parseAssetInfo, parseAssetInfoOnlyDenom, delay, - findPairAddress + findPairAddress, + calculatePriceByPool }; diff --git a/packages/oraidex-sync/src/query.ts b/packages/oraidex-sync/src/query.ts index 667ae09f..706b1e80 100644 --- a/packages/oraidex-sync/src/query.ts +++ b/packages/oraidex-sync/src/query.ts @@ -77,7 +77,7 @@ async function simulateSwapPricePair( offerAmount: tenAmountInDecimalSix, operations }); - return (parseInt(data.amount) / priceDecimals / 10).toString(); // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit + return (parseInt(data.amount) / priceDecimals).toString(); // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit } catch (error) { console.log(`Error when trying to simulate swap with pair: ${JSON.stringify(pair)} using router: ${error}`); return "0"; // error case. Will be handled by the caller function diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index e7d77dae..d4b5dc96 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -4,7 +4,8 @@ import { findAssetInfoPathToUsdt, findMappedTargetedAssetInfo, extractUniqueAndFlatten, - findPairAddress + findPairAddress, + calculatePriceByPool } from "../src/helper"; import { pairs } from "../src/pairs"; import { @@ -124,4 +125,9 @@ describe("test-helper", () => { // assert expect(result).toEqual(expectedPairAddr); }); + + it("test-calculatePriceByPool", () => { + const result = calculatePriceByPool(BigInt(10305560305234), BigInt(10205020305234), 0); + console.log("result: ", result.toString()); + }); }); From 83e7062ae03ae2429e098ddb5c95d5fab7f25c42 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 15:45:04 +0700 Subject: [PATCH 40/75] changed pair prices to match with usdt / usdc --- packages/oraidex-server/src/index.ts | 13 +++++----- packages/oraidex-sync/src/helper.ts | 15 +++++++++-- packages/oraidex-sync/src/query.ts | 4 +-- packages/oraidex-sync/tests/helper.spec.ts | 29 +++++++++++++++++++++- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 3f8dd812..9a42e780 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -14,7 +14,9 @@ import { getAllPairInfos, parseAssetInfo, PairInfoData, - findPairAddress + findPairAddress, + simulateSwapPriceWithUsdt, + findUsdOraiInPair } from "@oraichain/oraidex-sync"; import cors from "cors"; import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; @@ -88,18 +90,15 @@ app.get("/tickers", async (req, res) => { parseAssetInfoOnlyDenom(info) === usdtCw20Address || parseAssetInfoOnlyDenom(info) === usdcCw20Address ); const currencies = hasUsdInPair ? pair.symbols.reverse() : pair.symbols; - const assetInfosForSimulation = hasUsdInPair ? pair.asset_infos : pair.asset_infos.reverse(); + const assetInfoForSimulation = findUsdOraiInPair(pair.asset_infos); try { // reverse because in pairs, we put base info as first index - const price = await simulateSwapPricePair( - assetInfosForSimulation as [AssetInfo, AssetInfo], - routerContract - ); + const price = await simulateSwapPriceWithUsdt(assetInfoForSimulation, routerContract); return { ticker_id: tickerId, base_currency: currencies[0], target_currency: currencies[1], - last_price: price, + last_price: price.amount, base_volume: "0", target_volume: "0", pool_id: pairAddr ?? "", diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index edb15f43..fb081141 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -1,6 +1,6 @@ import { Asset, AssetInfo, OraiswapRouterReadOnlyInterface, SwapOperation } from "@oraichain/oraidex-contracts-sdk"; import { pairs } from "./pairs"; -import { ORAI, tenAmountInDecimalSix, usdtCw20Address } from "./constants"; +import { ORAI, tenAmountInDecimalSix, usdcCw20Address, usdtCw20Address } from "./constants"; import { PairInfoData, PairMapping, PrefixSumHandlingData } from "./types"; function parseAssetInfo(info: AssetInfo): string { @@ -101,6 +101,16 @@ function calculatePriceByPool(offerPool: bigint, askPool: bigint, commissionRate return (askPool - (offerPool * askPool) / (offerPool + BigInt(tenAmountInDecimalSix))) * BigInt(1 - commissionRate); } +function findUsdOraiInPair(infos: [AssetInfo, AssetInfo]): AssetInfo { + const firstInfo = parseAssetInfoOnlyDenom(infos[0]); + const secondInfo = parseAssetInfoOnlyDenom(infos[1]); + if (firstInfo === usdtCw20Address || firstInfo === usdcCw20Address) return infos[1]; + if (secondInfo === usdtCw20Address || secondInfo === usdcCw20Address) return infos[0]; + if (firstInfo === ORAI) return infos[1]; + if (secondInfo === ORAI) return infos[0]; + return infos[0]; // default we calculate the first info in the asset info list +} + export { calculatePrefixSum, findMappedTargetedAssetInfo, @@ -111,5 +121,6 @@ export { parseAssetInfoOnlyDenom, delay, findPairAddress, - calculatePriceByPool + calculatePriceByPool, + findUsdOraiInPair }; diff --git a/packages/oraidex-sync/src/query.ts b/packages/oraidex-sync/src/query.ts index 706b1e80..be329bb5 100644 --- a/packages/oraidex-sync/src/query.ts +++ b/packages/oraidex-sync/src/query.ts @@ -49,8 +49,6 @@ async function simulateSwapPriceWithUsdt(info: AssetInfo, router: OraiswapRouter // adjust the query height to get data from the past const infoPath = findAssetInfoPathToUsdt(info); // usdt case, price is always 1 - if (infoPath.length === 1) - return { info, amount: tenAmountInDecimalSix.substring(0, tenAmountInDecimalSix.length - 1) }; const operations = generateSwapOperations(infoPath); if (operations.length === 0) return { info, amount: "0" }; // error case. Will be handled by the caller function try { @@ -58,7 +56,7 @@ async function simulateSwapPriceWithUsdt(info: AssetInfo, router: OraiswapRouter offerAmount: tenAmountInDecimalSix, operations }); - return { info, amount: data.amount.substring(0, data.amount.length - 1) }; // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit + return { info, amount: (parseInt(data.amount) / priceDecimals).toString() }; // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit } catch (error) { console.log(`Error when trying to simulate swap with asset info: ${JSON.stringify(info)} using router: ${error}`); return { info, amount: "0" }; // error case. Will be handled by the caller function diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index d4b5dc96..f1dd5870 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -5,14 +5,17 @@ import { findMappedTargetedAssetInfo, extractUniqueAndFlatten, findPairAddress, - calculatePriceByPool + calculatePriceByPool, + findUsdOraiInPair } from "../src/helper"; import { pairs } from "../src/pairs"; import { ORAI, airiCw20Adress, + atomIbcDenom, milkyCw20Address, scAtomCw20Address, + tronCw20Address, usdcCw20Address, usdtCw20Address } from "../src/constants"; @@ -130,4 +133,28 @@ describe("test-helper", () => { const result = calculatePriceByPool(BigInt(10305560305234), BigInt(10205020305234), 0); console.log("result: ", result.toString()); }); + + it.each<[[AssetInfo, AssetInfo], AssetInfo]>([ + [ + [{ native_token: { denom: ORAI } }, { native_token: { denom: atomIbcDenom } }], + { native_token: { denom: atomIbcDenom } } + ], + [ + [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdtCw20Address } }], + { native_token: { denom: ORAI } } + ], + [ + [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdcCw20Address } }], + { native_token: { denom: ORAI } } + ], + [ + [{ token: { contract_addr: tronCw20Address } }, { native_token: { denom: atomIbcDenom } }], + { token: { contract_addr: tronCw20Address } } + ] + ])("test-findUsdOraiInPair", (infos, expectedInfo) => { + // act + const result = findUsdOraiInPair(infos); + // assert + expect(result).toEqual(expectedInfo); + }); }); From fa1be16285c660535b3286706ecdd78962979566 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 16:06:09 +0700 Subject: [PATCH 41/75] fixed base, target & added random volume --- packages/oraidex-server/src/index.ts | 33 +++++++++++++--------- packages/oraidex-sync/src/helper.ts | 12 ++++---- packages/oraidex-sync/tests/helper.spec.ts | 19 ++++++++----- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 9a42e780..673b2901 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -33,6 +33,10 @@ dotenv.config(); const app = express(); app.use(cors()); +function getRandomNumber(min: number, max: number): string { + return (Math.floor(Math.random() * (max - min + 1)) + min).toString(); +} + let duckDb: DuckDb; const port = parseInt(process.env.PORT) || 2024; @@ -90,32 +94,33 @@ app.get("/tickers", async (req, res) => { parseAssetInfoOnlyDenom(info) === usdtCw20Address || parseAssetInfoOnlyDenom(info) === usdcCw20Address ); const currencies = hasUsdInPair ? pair.symbols.reverse() : pair.symbols; - const assetInfoForSimulation = findUsdOraiInPair(pair.asset_infos); + const { base, target } = findUsdOraiInPair(pair.asset_infos); + const targetStr = parseAssetInfoOnlyDenom(target); try { // reverse because in pairs, we put base info as first index - const price = await simulateSwapPriceWithUsdt(assetInfoForSimulation, routerContract); + const price = await simulateSwapPriceWithUsdt(target, routerContract); return { ticker_id: tickerId, - base_currency: currencies[0], - target_currency: currencies[1], + base_currency: base, + target_currency: targetStr, last_price: price.amount, - base_volume: "0", - target_volume: "0", + base_volume: getRandomNumber(1000, 9999), // TODO: remove random + target_volume: getRandomNumber(1000, 9999), pool_id: pairAddr ?? "", - base: currencies[0], + base: base, target: currencies[1] } as TickerInfo; } catch (error) { return { ticker_id: tickerId, - base_currency: currencies[0], - target_currency: currencies[1], - last_price: "0", - base_volume: "0", - target_volume: "0", + base_currency: base, + target_currency: targetStr, + last_price: getRandomNumber(1000, 9999), + base_volume: getRandomNumber(1000, 9999), + target_volume: getRandomNumber(1000, 9999), pool_id: pairAddr ?? "", - base: currencies[0], - target: currencies[1] + base: base, + target: targetStr }; } }) diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index fb081141..a0760beb 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -101,14 +101,14 @@ function calculatePriceByPool(offerPool: bigint, askPool: bigint, commissionRate return (askPool - (offerPool * askPool) / (offerPool + BigInt(tenAmountInDecimalSix))) * BigInt(1 - commissionRate); } -function findUsdOraiInPair(infos: [AssetInfo, AssetInfo]): AssetInfo { +function findUsdOraiInPair(infos: [AssetInfo, AssetInfo]): { base: string; target: AssetInfo } { const firstInfo = parseAssetInfoOnlyDenom(infos[0]); const secondInfo = parseAssetInfoOnlyDenom(infos[1]); - if (firstInfo === usdtCw20Address || firstInfo === usdcCw20Address) return infos[1]; - if (secondInfo === usdtCw20Address || secondInfo === usdcCw20Address) return infos[0]; - if (firstInfo === ORAI) return infos[1]; - if (secondInfo === ORAI) return infos[0]; - return infos[0]; // default we calculate the first info in the asset info list + if (firstInfo === usdtCw20Address || firstInfo === usdcCw20Address) return { base: firstInfo, target: infos[1] }; + if (secondInfo === usdtCw20Address || secondInfo === usdcCw20Address) return { base: secondInfo, target: infos[0] }; + if (firstInfo === ORAI) return { base: firstInfo, target: infos[1] }; + if (secondInfo === ORAI) return { base: secondInfo, target: infos[0] }; + return { base: secondInfo, target: infos[0] }; // default we calculate the first info in the asset info list } export { diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index f1dd5870..49812e26 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -134,27 +134,32 @@ describe("test-helper", () => { console.log("result: ", result.toString()); }); - it.each<[[AssetInfo, AssetInfo], AssetInfo]>([ + it.each<[[AssetInfo, AssetInfo], AssetInfo, string]>([ [ [{ native_token: { denom: ORAI } }, { native_token: { denom: atomIbcDenom } }], - { native_token: { denom: atomIbcDenom } } + { native_token: { denom: atomIbcDenom } }, + ORAI ], [ [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdtCw20Address } }], - { native_token: { denom: ORAI } } + { native_token: { denom: ORAI } }, + usdtCw20Address ], [ [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdcCw20Address } }], - { native_token: { denom: ORAI } } + { native_token: { denom: ORAI } }, + usdcCw20Address ], [ [{ token: { contract_addr: tronCw20Address } }, { native_token: { denom: atomIbcDenom } }], - { token: { contract_addr: tronCw20Address } } + { token: { contract_addr: tronCw20Address } }, + atomIbcDenom ] - ])("test-findUsdOraiInPair", (infos, expectedInfo) => { + ])("test-findUsdOraiInPair", (infos, expectedInfo, expectedBase) => { // act const result = findUsdOraiInPair(infos); // assert - expect(result).toEqual(expectedInfo); + expect(result.target).toEqual(expectedInfo); + expect(result.base).toEqual(expectedBase); }); }); From fb03ec398a8114ff41fd0b6a76665dcc7c54480c Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 16:11:13 +0700 Subject: [PATCH 42/75] base orai to uppercase ORAI --- packages/oraidex-sync/src/helper.ts | 4 ++-- packages/oraidex-sync/tests/helper.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index a0760beb..b00c31cb 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -106,8 +106,8 @@ function findUsdOraiInPair(infos: [AssetInfo, AssetInfo]): { base: string; targe const secondInfo = parseAssetInfoOnlyDenom(infos[1]); if (firstInfo === usdtCw20Address || firstInfo === usdcCw20Address) return { base: firstInfo, target: infos[1] }; if (secondInfo === usdtCw20Address || secondInfo === usdcCw20Address) return { base: secondInfo, target: infos[0] }; - if (firstInfo === ORAI) return { base: firstInfo, target: infos[1] }; - if (secondInfo === ORAI) return { base: secondInfo, target: infos[0] }; + if (firstInfo === ORAI) return { base: firstInfo.toUpperCase(), target: infos[1] }; + if (secondInfo === ORAI) return { base: secondInfo.toUpperCase(), target: infos[0] }; return { base: secondInfo, target: infos[0] }; // default we calculate the first info in the asset info list } diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index 49812e26..5b222417 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -138,7 +138,7 @@ describe("test-helper", () => { [ [{ native_token: { denom: ORAI } }, { native_token: { denom: atomIbcDenom } }], { native_token: { denom: atomIbcDenom } }, - ORAI + ORAI.toUpperCase() ], [ [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdtCw20Address } }], From ac076d8d25d361142107a9749b4eca414c36f26a Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 16:27:18 +0700 Subject: [PATCH 43/75] changed display price to using toDisplay from oraidex --- packages/oraidex-sync/src/constants.ts | 3 +- packages/oraidex-sync/src/helper.ts | 44 ++++++++++- packages/oraidex-sync/src/query.ts | 8 +- packages/oraidex-sync/tests/helper.spec.ts | 87 +++++++++++++++++++++- 4 files changed, 133 insertions(+), 9 deletions(-) diff --git a/packages/oraidex-sync/src/constants.ts b/packages/oraidex-sync/src/constants.ts index cdfa22d8..bc373754 100644 --- a/packages/oraidex-sync/src/constants.ts +++ b/packages/oraidex-sync/src/constants.ts @@ -11,4 +11,5 @@ export const usdcCw20Address = "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd export const atomIbcDenom = "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"; export const osmosisIbcDenom = "ibc/9C4DCD21B48231D0BC2AC3D1B74A864746B37E4292694C93C617324250D002FC"; export const tenAmountInDecimalSix = "10000000"; -export const priceDecimals = 10 ** 7; +export const truncDecimals = 6; +export const atomic = 10 ** truncDecimals; diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index b00c31cb..4f916c72 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -1,8 +1,50 @@ import { Asset, AssetInfo, OraiswapRouterReadOnlyInterface, SwapOperation } from "@oraichain/oraidex-contracts-sdk"; import { pairs } from "./pairs"; -import { ORAI, tenAmountInDecimalSix, usdcCw20Address, usdtCw20Address } from "./constants"; +import { ORAI, atomic, tenAmountInDecimalSix, truncDecimals, usdcCw20Address, usdtCw20Address } from "./constants"; import { PairInfoData, PairMapping, PrefixSumHandlingData } from "./types"; +export const validateNumber = (amount: number | string): number => { + if (typeof amount === "string") return validateNumber(Number(amount)); + if (Number.isNaN(amount) || !Number.isFinite(amount)) return 0; + return amount; +}; + +// decimals always >= 6 +export const toAmount = (amount: number | string, decimals = 6): bigint => { + const validatedAmount = validateNumber(amount); + return BigInt(Math.trunc(validatedAmount * atomic)) * BigInt(10 ** (decimals - truncDecimals)); +}; + +/** + * Converts a fraction to its equivalent decimal value as a number. + * + * @param {bigint} numerator - The numerator of the fraction + * @param {bigint} denominator - The denominator of the fraction + * @return {number} - The decimal value equivalent to the input fraction, returned as a number. + */ +export const toDecimal = (numerator: bigint, denominator: bigint): number => { + if (denominator === BigInt(0)) return 0; + return toDisplay((numerator * BigInt(10 ** 6)) / denominator, 6); +}; + +/** + * Convert the amount to be displayed on the user interface. + * + * @param {string|bigint} amount - The amount to be converted. + * @param {number} sourceDecimals - The number of decimal places in the original `amount`. + * @param {number} desDecimals - The number of decimal places in the `amount` after conversion. + * @return {number} The value of `amount` after conversion. + */ +export const toDisplay = (amount: string | bigint, sourceDecimals = 6, desDecimals = 6): number => { + if (!amount) return 0; + // guarding conditions to prevent crashing + const validatedAmount = typeof amount === "string" ? BigInt(amount || "0") : amount; + const displayDecimals = Math.min(truncDecimals, desDecimals); + const returnAmount = validatedAmount / BigInt(10 ** (sourceDecimals - displayDecimals)); + // save calculation by using cached atomic + return Number(returnAmount) / (displayDecimals === truncDecimals ? atomic : 10 ** displayDecimals); +}; + function parseAssetInfo(info: AssetInfo): string { // if ("native_token" in info) return info.native_token.denom; // return info.token.contract_addr; diff --git a/packages/oraidex-sync/src/query.ts b/packages/oraidex-sync/src/query.ts index be329bb5..e329f23a 100644 --- a/packages/oraidex-sync/src/query.ts +++ b/packages/oraidex-sync/src/query.ts @@ -8,8 +8,8 @@ import { Asset, AssetInfo } from "@oraichain/oraidex-contracts-sdk"; import { MulticallReadOnlyInterface } from "@oraichain/common-contracts-sdk"; import { fromBinary, toBinary } from "@cosmjs/cosmwasm-stargate"; import { pairs } from "./pairs"; -import { findAssetInfoPathToUsdt, generateSwapOperations } from "./helper"; -import { priceDecimals, tenAmountInDecimalSix } from "./constants"; +import { findAssetInfoPathToUsdt, generateSwapOperations, toDisplay } from "./helper"; +import { tenAmountInDecimalSix } from "./constants"; async function getPoolInfos(pairs: PairInfo[], multicall: MulticallReadOnlyInterface): Promise { // adjust the query height to get data from the past @@ -56,7 +56,7 @@ async function simulateSwapPriceWithUsdt(info: AssetInfo, router: OraiswapRouter offerAmount: tenAmountInDecimalSix, operations }); - return { info, amount: (parseInt(data.amount) / priceDecimals).toString() }; // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit + return { info, amount: toDisplay(data.amount, 7).toString() }; // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit } catch (error) { console.log(`Error when trying to simulate swap with asset info: ${JSON.stringify(info)} using router: ${error}`); return { info, amount: "0" }; // error case. Will be handled by the caller function @@ -75,7 +75,7 @@ async function simulateSwapPricePair( offerAmount: tenAmountInDecimalSix, operations }); - return (parseInt(data.amount) / priceDecimals).toString(); // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit + return toDisplay(data.amount, 7).toString(); // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit } catch (error) { console.log(`Error when trying to simulate swap with pair: ${JSON.stringify(pair)} using router: ${error}`); return "0"; // error case. Will be handled by the caller function diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index 5b222417..caa2e284 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -1,4 +1,4 @@ -import { Asset, AssetInfo } from "@oraichain/oraidex-contracts-sdk"; +import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; import { calculatePrefixSum, findAssetInfoPathToUsdt, @@ -6,7 +6,10 @@ import { extractUniqueAndFlatten, findPairAddress, calculatePriceByPool, - findUsdOraiInPair + findUsdOraiInPair, + toAmount, + toDisplay, + toDecimal } from "../src/helper"; import { pairs } from "../src/pairs"; import { @@ -22,6 +25,84 @@ import { import { PairInfoData } from "../src/types"; describe("test-helper", () => { + describe("bigint", () => { + describe("toAmount", () => { + it("toAmount-percent", () => { + const bondAmount = BigInt(1000); + const percentValue = (toAmount(0.3, 6) * bondAmount) / BigInt(100000000); + expect(percentValue.toString()).toBe("3"); + }); + + it.each([ + [6000, 18, "6000000000000000000000"], + [2000000, 18, "2000000000000000000000000"], + [6000.5043177, 6, "6000504317"], + [6000.504317725654, 6, "6000504317"], + [0.0006863532, 6, "686"] + ])( + "toAmount number %.7f with decimal %d should return %s", + (amount: number, decimal: number, expectedAmount: string) => { + const res = toAmount(amount, decimal).toString(); + expect(res).toBe(expectedAmount); + } + ); + }); + + describe("toDisplay", () => { + it.each([ + ["1000", 6, "0.001", 6], + ["454136345353413531", 15, "454.136345", 6], + ["454136345353413531", 15, "454.13", 2], + ["100000000000000", 18, "0.0001", 6] + ])( + "toDisplay number %d with decimal %d should return %s", + (amount: string, decimal: number, expectedAmount: string, desDecimal: number) => { + const res = toDisplay(amount, decimal, desDecimal).toString(); + expect(res).toBe(expectedAmount); + } + ); + }); + + describe("toDecimal", () => { + it("toDecimal-happy-path", async () => { + const numerator = BigInt(6); + const denominator = BigInt(3); + const res = toDecimal(numerator, denominator); + expect(res).toBe(2); + }); + + it("should return 0 when denominator is zero", async () => { + const numerator = BigInt(123456); + const denominator = BigInt(0); + expect(toDecimal(numerator, denominator)).toBe(0); + }); + + it("should correctly convert a fraction into its equivalent decimal value", () => { + const numerator = BigInt(1); + const denominator = BigInt(3); + + // Convert the fraction to its decimal value using toDecimal. + const decimalValue = toDecimal(numerator, denominator); + // Expect the decimal value to be equal to the expected value. + expect(decimalValue).toBeCloseTo(0.333333, 6); + }); + + it.each([ + [BigInt(1), BigInt(3), 0.333333, 6], + [BigInt(1), BigInt(3), 0.3333, 4], + [BigInt(1), BigInt(2), 0.5, 6] + ])( + "should correctly convert a fraction into its equivalent decimal value", + (numerator, denominator, expectedDecValue, desDecimal) => { + // Convert the fraction to its decimal value using toDecimal. + const decimalValue = toDecimal(numerator, denominator); + // Expect the decimal value to be equal to the expected value. + expect(decimalValue).toBeCloseTo(expectedDecValue, desDecimal); + } + ); + }); + }); + it.each<[AssetInfo, number]>([ [{ token: { contract_addr: usdtCw20Address } }, 2], [{ token: { contract_addr: usdcCw20Address } }, 1], @@ -131,7 +212,7 @@ describe("test-helper", () => { it("test-calculatePriceByPool", () => { const result = calculatePriceByPool(BigInt(10305560305234), BigInt(10205020305234), 0); - console.log("result: ", result.toString()); + expect(result).toEqual(9902432); }); it.each<[[AssetInfo, AssetInfo], AssetInfo, string]>([ From 81ff60fc38f7ab3e7831a89ff5568a7d84a9ea8d Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 16:54:56 +0700 Subject: [PATCH 44/75] fixed base & target error --- lp_ops_data.json | 1 + packages/oraidex-server/src/index.ts | 40 ++++++++-------------- packages/oraidex-sync/src/helper.ts | 18 ++++++---- packages/oraidex-sync/tests/helper.spec.ts | 12 +++---- 4 files changed, 34 insertions(+), 37 deletions(-) create mode 100644 lp_ops_data.json diff --git a/lp_ops_data.json b/lp_ops_data.json new file mode 100644 index 00000000..099dc724 --- /dev/null +++ b/lp_ops_data.json @@ -0,0 +1 @@ +[{"txhash":"foo","timestamp":"2023-07-17T09:54:37.896Z","firstTokenAmount":1,"firstTokenDenom":"orai","secondTokenAmount":2,"secondTokenDenom":"atom","txCreator":"foobar","opType":"withdraw"}] \ No newline at end of file diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 673b2901..ce26d5ff 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -93,36 +93,26 @@ app.get("/tickers", async (req, res) => { (info) => parseAssetInfoOnlyDenom(info) === usdtCw20Address || parseAssetInfoOnlyDenom(info) === usdcCw20Address ); - const currencies = hasUsdInPair ? pair.symbols.reverse() : pair.symbols; - const { base, target } = findUsdOraiInPair(pair.asset_infos); - const targetStr = parseAssetInfoOnlyDenom(target); + const { baseIndex, targetIndex, target } = findUsdOraiInPair(pair.asset_infos); + let tickerInfo: TickerInfo = { + ticker_id: tickerId, + base_currency: symbols[baseIndex], + target_currency: symbols[targetIndex], + last_price: "", + base_volume: getRandomNumber(1000, 9999), // TODO: remove random + target_volume: getRandomNumber(1000, 9999), + pool_id: pairAddr ?? "", + base: symbols[baseIndex], + target: symbols[targetIndex] + }; try { // reverse because in pairs, we put base info as first index const price = await simulateSwapPriceWithUsdt(target, routerContract); - return { - ticker_id: tickerId, - base_currency: base, - target_currency: targetStr, - last_price: price.amount, - base_volume: getRandomNumber(1000, 9999), // TODO: remove random - target_volume: getRandomNumber(1000, 9999), - pool_id: pairAddr ?? "", - base: base, - target: currencies[1] - } as TickerInfo; + tickerInfo.last_price = price.amount; } catch (error) { - return { - ticker_id: tickerId, - base_currency: base, - target_currency: targetStr, - last_price: getRandomNumber(1000, 9999), - base_volume: getRandomNumber(1000, 9999), - target_volume: getRandomNumber(1000, 9999), - pool_id: pairAddr ?? "", - base: base, - target: targetStr - }; + tickerInfo.last_price = "0"; } + return tickerInfo; }) ) ).map((result) => { diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index 4f916c72..c7085a01 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -143,14 +143,20 @@ function calculatePriceByPool(offerPool: bigint, askPool: bigint, commissionRate return (askPool - (offerPool * askPool) / (offerPool + BigInt(tenAmountInDecimalSix))) * BigInt(1 - commissionRate); } -function findUsdOraiInPair(infos: [AssetInfo, AssetInfo]): { base: string; target: AssetInfo } { +function findUsdOraiInPair(infos: [AssetInfo, AssetInfo]): { + baseIndex: number; + targetIndex: number; + target: AssetInfo; +} { const firstInfo = parseAssetInfoOnlyDenom(infos[0]); const secondInfo = parseAssetInfoOnlyDenom(infos[1]); - if (firstInfo === usdtCw20Address || firstInfo === usdcCw20Address) return { base: firstInfo, target: infos[1] }; - if (secondInfo === usdtCw20Address || secondInfo === usdcCw20Address) return { base: secondInfo, target: infos[0] }; - if (firstInfo === ORAI) return { base: firstInfo.toUpperCase(), target: infos[1] }; - if (secondInfo === ORAI) return { base: secondInfo.toUpperCase(), target: infos[0] }; - return { base: secondInfo, target: infos[0] }; // default we calculate the first info in the asset info list + if (firstInfo === usdtCw20Address || firstInfo === usdcCw20Address) + return { baseIndex: 0, targetIndex: 1, target: infos[1] }; + if (secondInfo === usdtCw20Address || secondInfo === usdcCw20Address) + return { baseIndex: 1, targetIndex: 0, target: infos[0] }; + if (firstInfo === ORAI) return { baseIndex: 0, targetIndex: 1, target: infos[1] }; + if (secondInfo === ORAI) return { baseIndex: 1, targetIndex: 0, target: infos[0] }; + return { baseIndex: 1, targetIndex: 0, target: infos[0] }; // default we calculate the first info in the asset info list } export { diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index caa2e284..676ac44e 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -215,32 +215,32 @@ describe("test-helper", () => { expect(result).toEqual(9902432); }); - it.each<[[AssetInfo, AssetInfo], AssetInfo, string]>([ + it.each<[[AssetInfo, AssetInfo], AssetInfo, number]>([ [ [{ native_token: { denom: ORAI } }, { native_token: { denom: atomIbcDenom } }], { native_token: { denom: atomIbcDenom } }, - ORAI.toUpperCase() + 0 ], [ [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdtCw20Address } }], { native_token: { denom: ORAI } }, - usdtCw20Address + 1 ], [ [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdcCw20Address } }], { native_token: { denom: ORAI } }, - usdcCw20Address + 1 ], [ [{ token: { contract_addr: tronCw20Address } }, { native_token: { denom: atomIbcDenom } }], { token: { contract_addr: tronCw20Address } }, - atomIbcDenom + 1 ] ])("test-findUsdOraiInPair", (infos, expectedInfo, expectedBase) => { // act const result = findUsdOraiInPair(infos); // assert expect(result.target).toEqual(expectedInfo); - expect(result.base).toEqual(expectedBase); + expect(result.baseIndex).toEqual(expectedBase); }); }); From a7acc3c8d2f5035336c78315309782cd3009caad Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 16:55:09 +0700 Subject: [PATCH 45/75] removed test data --- lp_ops_data.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 lp_ops_data.json diff --git a/lp_ops_data.json b/lp_ops_data.json deleted file mode 100644 index 099dc724..00000000 --- a/lp_ops_data.json +++ /dev/null @@ -1 +0,0 @@ -[{"txhash":"foo","timestamp":"2023-07-17T09:54:37.896Z","firstTokenAmount":1,"firstTokenDenom":"orai","secondTokenAmount":2,"secondTokenDenom":"atom","txCreator":"foobar","opType":"withdraw"}] \ No newline at end of file From 259e2e3a7977262cf5fe01f1895c8dd7ff7a32ba Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 17:29:59 +0700 Subject: [PATCH 46/75] added type lp & volume & fixed test cases --- packages/oraidex-sync/src/tx-parsing.ts | 6 ++++++ packages/oraidex-sync/src/types.ts | 4 ++++ packages/oraidex-sync/tests/db.spec.ts | 4 ++++ packages/oraidex-sync/tests/helper.spec.ts | 2 +- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index b4093ed2..a3ef259e 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -86,7 +86,9 @@ function extractSwapOperations(txhash: string, timestamp: string, events: readon timestamp, offerDenom: offerDenoms[i], offerAmount: offerAmounts[i], + offerVolume: 0, askDenom: askDenoms[i], + askVolume: 0, returnAmount: returnAmounts[i], taxAmount: parseInt(taxAmounts[i]), commissionAmount: parseInt(commissionAmounts[i]), @@ -109,8 +111,10 @@ function extractMsgProvideLiquidity( txhash, timestamp, firstTokenAmount: parseInt(firstAsset.amount), + firstTokenLp: 0, firstTokenDenom: parseAssetInfo(firstAsset.info), secondTokenAmount: parseInt(secAsset.amount), + secondTokenLp: 0, secondTokenDenom: parseAssetInfo(secAsset.info), txCreator, opType: "provide" @@ -147,8 +151,10 @@ function extractMsgWithdrawLiquidity( txhash, timestamp, firstTokenAmount: parseInt(assets[0]), + firstTokenLp: 0, firstTokenDenom: assets[1], secondTokenAmount: parseInt(assets[2]), + secondTokenLp: 0, secondTokenDenom: assets[3], txCreator, opType: "withdraw" diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index dbe69960..4312746a 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -20,7 +20,9 @@ export type BasicTxData = { export type SwapOperationData = BasicTxData & { offerDenom: string; offerAmount: string; + offerVolume: number; askDenom: string; + askVolume: number; returnAmount: string; taxAmount: number; commissionAmount: number; @@ -52,8 +54,10 @@ export type LiquidityOpType = "provide" | "withdraw"; export type ProvideLiquidityOperationData = BasicTxData & { firstTokenAmount: number; + firstTokenLp: number; firstTokenDenom: string; secondTokenAmount: number; + secondTokenLp: number; secondTokenDenom: string; txCreator: string; opType: LiquidityOpType; diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index 75bccad1..347f3039 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -16,8 +16,10 @@ describe("test-duckdb", () => { txhash: "foo", timestamp: new Date().toISOString(), firstTokenAmount: "abcd" as any, + firstTokenLp: 0, firstTokenDenom: "orai", secondTokenAmount: 2, + secondTokenLp: 0, secondTokenDenom: "atom", txCreator: "foobar", opType: "provide" @@ -33,8 +35,10 @@ describe("test-duckdb", () => { txhash: "foo", timestamp: new Date().toISOString(), firstTokenAmount: 1, + firstTokenLp: 0, firstTokenDenom: "orai", secondTokenAmount: 2, + secondTokenLp: 0, secondTokenDenom: "atom", txCreator: "foobar", opType: "withdraw" diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index 676ac44e..15c5d134 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -212,7 +212,7 @@ describe("test-helper", () => { it("test-calculatePriceByPool", () => { const result = calculatePriceByPool(BigInt(10305560305234), BigInt(10205020305234), 0); - expect(result).toEqual(9902432); + expect(result).toEqual(BigInt(9902432)); }); it.each<[[AssetInfo, AssetInfo], AssetInfo, number]>([ From ff7a732f5576d1fb36d09eda3a38b0f657fa8a5c Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Mon, 17 Jul 2023 23:18:15 +0700 Subject: [PATCH 47/75] added swap ops prefix sum & updated db test cases --- packages/oraidex-sync/src/db.ts | 4 +- packages/oraidex-sync/src/helper.ts | 1 - packages/oraidex-sync/src/index.ts | 54 ++++++++++++++++++------- packages/oraidex-sync/src/tx-parsing.ts | 19 ++++----- packages/oraidex-sync/src/types.ts | 4 +- packages/oraidex-sync/tests/db.spec.ts | 52 +++++++++++++++++++++++- 6 files changed, 103 insertions(+), 31 deletions(-) diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 3eca9b9b..7eaec05a 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -92,8 +92,8 @@ export class DuckDb { await this.insertBulkData(ops, "price_infos", false, `price_infos-${Math.random() * 1000}`); } - async querySwapOps() { - return this.conn.all("SELECT count(*) from swap_ops_data"); + async queryLatestTimestampSwapOps() { + return this.conn.all("SELECT offerVolume, askVolume, timestamp from swap_ops_data order by timestamp desc limit 1"); } async queryLpOps() { diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index c7085a01..af4242ea 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -71,7 +71,6 @@ function calculatePrefixSum(initialAmount: number, handlingData: PrefixSumHandli prefixSumObj[`temp-${data.denom}`] += data.amount; data.amount = prefixSumObj[`temp-${data.denom}`]; } - console.log("new results: ", handlingData); return handlingData; } diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 2380233c..cb5004fd 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -22,7 +22,7 @@ import { } from "./types"; import { MulticallQueryClient } from "@oraichain/common-contracts-sdk"; import { PoolResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; -import { extractUniqueAndFlatten } from "./helper"; +import { calculatePrefixSum, extractUniqueAndFlatten } from "./helper"; import { getAllPairInfos, getPoolInfos, simulateSwapPriceWithUsdt } from "./query"; class WriteOrders extends WriteData { @@ -49,8 +49,8 @@ class WriteOrders extends WriteData { ]); } - private async querySwapOps(): Promise { - return this.duckDb.querySwapOps() as Promise; + private async queryLatestSwapOps(): Promise { + return this.duckDb.queryLatestTimestampSwapOps() as Promise; } private async queryLpOps(): Promise { @@ -79,15 +79,39 @@ class WriteOrders extends WriteData { } const { txs, offset: newOffset, queryTags } = chunk as Txs; console.log("new offset: ", newOffset); - const result = parseTxs(txs); + let result = parseTxs(txs); + + // collect the latest offer & ask volume to accumulate the results + const swapOps = await this.queryLatestSwapOps(); + + const prefixSum = calculatePrefixSum( + 100000000, + result.swapOpsData + .map((data) => [ + { denom: data.offerDenom, amount: data.offerAmount }, + { denom: data.askDenom, amount: data.returnAmount } + ]) + .flat() + ); + let newSwapOps: SwapOperationData[] = result.swapOpsData; + let prefixSumIndex = 0; + if (prefixSum.length !== result.swapOpsData.length * 2) { + throw Error("Prefix sum length does not match the swap ops length"); + } + for (let i = 0; i < result.swapOpsData.length; i++) { + if (newSwapOps[i].offerDenom === prefixSum[prefixSumIndex].denom) { + newSwapOps[i].offerVolume = prefixSum[prefixSumIndex].amount; + } + if (newSwapOps[i].askDenom === prefixSum[prefixSumIndex + 1].denom) { + newSwapOps[i].askVolume = prefixSum[prefixSumIndex + 1].amount; + } + prefixSumIndex += 2; + } // insert txs await this.duckDb.insertHeightSnapshot(newOffset); await this.insertParsedTxs(result); - const swapOps = await this.querySwapOps(); const lpOps = await this.queryLpOps(); - - console.log("swap ops: ", swapOps); console.log("lp ops: ", lpOps); } catch (error) { console.log("error processing data: ", error); @@ -187,9 +211,9 @@ class OraiDexSync { offset: currentInd, rpcUrl: this.rpcUrl, queryTags: [], - limit: 1, + limit: 100, maxThreadLevel: 1, - interval: 1000 + interval: 5000 }).pipe(new WriteOrders(this.duckDb, initialData)); } catch (error) { console.log("error in start: ", error); @@ -197,13 +221,13 @@ class OraiDexSync { } } -// const start = async () => { -// const duckDb = await DuckDb.create("oraidex-sync-data"); -// const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); -// await oraidexSync.sync(); -// }; +const start = async () => { + const duckDb = await DuckDb.create("oraidex-sync-data"); + const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); + await oraidexSync.sync(); +}; -// start(); +start(); export { OraiDexSync }; diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index a3ef259e..5887715e 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -10,13 +10,14 @@ import { MsgType, OraiswapPairCw20HookMsg, OraiswapRouterCw20HookMsg, + PrefixSumHandlingData, ProvideLiquidityOperationData, SwapOperationData, TxAnlysisResult, WithdrawLiquidityOperationData } from "./types"; import { Log } from "@cosmjs/stargate/build/logs"; -import { parseAssetInfo } from "./helper"; +import { calculatePrefixSum, parseAssetInfo } from "./helper"; function parseWasmEvents(events: readonly Event[]): (readonly Attribute[])[] { return events.filter((event) => event.type === "wasm").map((event) => event.attributes); @@ -85,11 +86,11 @@ function extractSwapOperations(txhash: string, timestamp: string, events: readon txhash, timestamp, offerDenom: offerDenoms[i], - offerAmount: offerAmounts[i], - offerVolume: 0, + offerAmount: parseInt(offerAmounts[i]), + offerVolume: parseInt(offerAmounts[i]), askDenom: askDenoms[i], - askVolume: 0, - returnAmount: returnAmounts[i], + askVolume: parseInt(returnAmounts[i]), + returnAmount: parseInt(returnAmounts[i]), taxAmount: parseInt(taxAmounts[i]), commissionAmount: parseInt(commissionAmounts[i]), spreadAmount: parseInt(spreadAmounts[i]) @@ -111,10 +112,10 @@ function extractMsgProvideLiquidity( txhash, timestamp, firstTokenAmount: parseInt(firstAsset.amount), - firstTokenLp: 0, + firstTokenLp: parseInt(firstAsset.amount), firstTokenDenom: parseAssetInfo(firstAsset.info), secondTokenAmount: parseInt(secAsset.amount), - secondTokenLp: 0, + secondTokenLp: parseInt(secAsset.amount), secondTokenDenom: parseAssetInfo(secAsset.info), txCreator, opType: "provide" @@ -151,10 +152,10 @@ function extractMsgWithdrawLiquidity( txhash, timestamp, firstTokenAmount: parseInt(assets[0]), - firstTokenLp: 0, + firstTokenLp: parseInt(assets[0]), firstTokenDenom: assets[1], secondTokenAmount: parseInt(assets[2]), - secondTokenLp: 0, + secondTokenLp: parseInt(assets[2]), secondTokenDenom: assets[3], txCreator, opType: "withdraw" diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 4312746a..f5410e10 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -19,11 +19,11 @@ export type BasicTxData = { export type SwapOperationData = BasicTxData & { offerDenom: string; - offerAmount: string; + offerAmount: number; offerVolume: number; askDenom: string; askVolume: number; - returnAmount: string; + returnAmount: number; taxAmount: number; commissionAmount: number; spreadAmount: number; diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index 347f3039..3ac54a47 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -4,10 +4,45 @@ describe("test-duckdb", () => { let duckDb: DuckDb; beforeAll(async () => { // fixture - duckDb = await DuckDb.create("oraidex-sync-data-test"); + duckDb = await DuckDb.create(":memory:"); await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); }); + it("test-duckdb-queryLatestTimestampSwapOps-should-return-the-latest-timestamp-row", async () => { + await duckDb.insertSwapOps([ + { + txhash: "foo", + timestamp: new Date(1689610068000).toISOString(), + offerAmount: 1, + offerDenom: "0", + offerVolume: 1, + askDenom: "orai", + askVolume: 2, + returnAmount: 0, + taxAmount: 0, + commissionAmount: 0, + spreadAmount: 0 + }, + { + txhash: "foo", + timestamp: new Date(1589610068000).toISOString(), + offerAmount: 1, + offerDenom: "0", + offerVolume: 1, + askDenom: "orai", + askVolume: 2, + returnAmount: 0, + taxAmount: 0, + commissionAmount: 0, + spreadAmount: 0 + } + ]); + const queryResult = await duckDb.queryLatestTimestampSwapOps(); + expect(new Date(queryResult[0].timestamp).toISOString()).toEqual( + new Date("2023-07-17T16:07:48.000Z").toISOString() + ); + }); + it("test-duckdb-insert-bulk-should-throw-error-when-wrong-data", async () => { // act & test await expect( @@ -30,10 +65,11 @@ describe("test-duckdb", () => { it("test-duckdb-insert-bulk-should-pass-and-can-query", async () => { // act & test + const newDate = new Date().toISOString(); await duckDb.insertLpOps([ { txhash: "foo", - timestamp: new Date().toISOString(), + timestamp: newDate, firstTokenAmount: 1, firstTokenLp: 0, firstTokenDenom: "orai", @@ -46,5 +82,17 @@ describe("test-duckdb", () => { ]); const queryResult = await duckDb.queryLpOps(); console.log("query result: ", queryResult); + expect(queryResult[0]).toEqual({ + txhash: "foo", + timestamp: newDate, + firstTokenAmount: 1, + firstTokenLp: 0, + firstTokenDenom: "orai", + secondTokenAmount: 2, + secondTokenLp: 0, + secondTokenDenom: "atom", + txCreator: "foobar", + opType: "withdraw" + }); }); }); From 4a8a172c955e1e454ce232181c2b7b1eb1fa0a24 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 18 Jul 2023 14:26:14 +0700 Subject: [PATCH 48/75] fixed db --- packages/oraidex-sync/src/db.ts | 40 ++++++++++++++--- packages/oraidex-sync/src/index.ts | 32 +------------- packages/oraidex-sync/src/tx-parsing.ts | 39 ++++++++--------- packages/oraidex-sync/src/types.ts | 29 +++++++------ packages/oraidex-sync/tests/db.spec.ts | 57 +++++++++++-------------- 5 files changed, 96 insertions(+), 101 deletions(-) diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 7eaec05a..0558773f 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -1,5 +1,5 @@ import { Database, Connection } from "duckdb-async"; -import { PairInfoData, PriceInfo, SwapOperationData, WithdrawLiquidityOperationData } from "./types"; +import { PairInfoData, PriceInfo, SwapOperationData, TokenVolumeData, WithdrawLiquidityOperationData } from "./types"; import fs from "fs"; export class DuckDb { @@ -41,7 +41,16 @@ export class DuckDb { // swap operation table handling async createSwapOpsTable() { await this.conn.exec( - "CREATE TABLE IF NOT EXISTS swap_ops_data (txhash VARCHAR, timestamp TIMESTAMP, offerDenom VARCHAR, offerAmount UBIGINT, offerVolume UBIGINT, askDenom VARCHAR, askVolume UBIGINT, returnAmount UBIGINT, taxAmount UBIGINT, commissionAmount UBIGINT, spreadAmount UBIGINT)" + `CREATE TABLE IF NOT EXISTS swap_ops_data ( + askDenom VARCHAR, + commissionAmount UBIGINT, + offerAmount UBIGINT, + offerDenom VARCHAR, + returnAmount UBIGINT, + spreadAmount UBIGINT, + taxAmount UBIGINT, + timestamp TIMESTAMP, + txhash VARCHAR)` ); } @@ -58,7 +67,17 @@ export class DuckDb { await this.conn.exec("CREATE TYPE LPOPTYPE AS ENUM ('provide','withdraw');"); } await this.conn.exec( - "CREATE TABLE IF NOT EXISTS lp_ops_data (txhash VARCHAR, timestamp TIMESTAMP, firstTokenAmount UBIGINT, firstTokenLp UBIGINT, firstTokenDenom VARCHAR, secondTokenAmount UBIGINT, secondTokenLp UBIGINT, secondTokenDenom VARCHAR, txCreator VARCHAR, opType LPOPTYPE)" + `CREATE TABLE IF NOT EXISTS lp_ops_data ( + firstTokenAmount UBIGINT, + firstTokenDenom VARCHAR, + firstTokenLp UBIGINT, + opType LPOPTYPE, + secondTokenAmount UBIGINT, + secondTokenDenom VARCHAR, + secondTokenLp UBIGINT, + timestamp TIMESTAMP, + txCreator VARCHAR, + txhash VARCHAR)` ); } @@ -92,12 +111,21 @@ export class DuckDb { await this.insertBulkData(ops, "price_infos", false, `price_infos-${Math.random() * 1000}`); } - async queryLatestTimestampSwapOps() { - return this.conn.all("SELECT offerVolume, askVolume, timestamp from swap_ops_data order by timestamp desc limit 1"); + async queryAllVolume(denom: string): Promise { + // TODO: justify limit 1 here + const offerVolume = await this.conn.all( + `SELECT offerDenom as denom, sum(offerAmount) as volume from swap_ops_data where denom = '${denom}' group by denom limit 1;` + ); + const askVolume = await this.conn.all( + `SELECT askDenom as denom, sum(returnAmount) as volume from swap_ops_data where denom = '${denom}' group by denom limit 1;` + ); + if (offerVolume.length === 0 && askVolume.length === 0) return { denom, volume: 0 }; + if (offerVolume.length === 0) return askVolume[0] as TokenVolumeData; + return offerVolume[0] as TokenVolumeData; } async queryLpOps() { - return this.conn.all("SELECT count(*) from lp_ops_data"); + return this.conn.all("SELECT * from lp_ops_data"); } async queryPairInfos(): Promise { diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index cb5004fd..5160b693 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -18,7 +18,8 @@ import { WithdrawLiquidityOperationData, InitialData, PriceInfo, - PairInfoData + PairInfoData, + PrefixSumHandlingData } from "./types"; import { MulticallQueryClient } from "@oraichain/common-contracts-sdk"; import { PoolResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; @@ -49,10 +50,6 @@ class WriteOrders extends WriteData { ]); } - private async queryLatestSwapOps(): Promise { - return this.duckDb.queryLatestTimestampSwapOps() as Promise; - } - private async queryLpOps(): Promise { return this.duckDb.queryLpOps() as Promise; } @@ -82,31 +79,6 @@ class WriteOrders extends WriteData { let result = parseTxs(txs); // collect the latest offer & ask volume to accumulate the results - const swapOps = await this.queryLatestSwapOps(); - - const prefixSum = calculatePrefixSum( - 100000000, - result.swapOpsData - .map((data) => [ - { denom: data.offerDenom, amount: data.offerAmount }, - { denom: data.askDenom, amount: data.returnAmount } - ]) - .flat() - ); - let newSwapOps: SwapOperationData[] = result.swapOpsData; - let prefixSumIndex = 0; - if (prefixSum.length !== result.swapOpsData.length * 2) { - throw Error("Prefix sum length does not match the swap ops length"); - } - for (let i = 0; i < result.swapOpsData.length; i++) { - if (newSwapOps[i].offerDenom === prefixSum[prefixSumIndex].denom) { - newSwapOps[i].offerVolume = prefixSum[prefixSumIndex].amount; - } - if (newSwapOps[i].askDenom === prefixSum[prefixSumIndex + 1].denom) { - newSwapOps[i].askVolume = prefixSum[prefixSumIndex + 1].amount; - } - prefixSumIndex += 2; - } // insert txs await this.duckDb.insertHeightSnapshot(newOffset); await this.insertParsedTxs(result); diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 5887715e..364ce840 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -17,7 +17,7 @@ import { WithdrawLiquidityOperationData } from "./types"; import { Log } from "@cosmjs/stargate/build/logs"; -import { calculatePrefixSum, parseAssetInfo } from "./helper"; +import { calculatePrefixSum, parseAssetInfo, parseAssetInfoOnlyDenom } from "./helper"; function parseWasmEvents(events: readonly Event[]): (readonly Attribute[])[] { return events.filter((event) => event.type === "wasm").map((event) => event.attributes); @@ -80,20 +80,17 @@ function extractSwapOperations(txhash: string, timestamp: string, events: readon } } } - // TODO: check length of above data should be equal because otherwise we would miss information for (let i = 0; i < askDenoms.length; i++) { swapData.push({ - txhash, - timestamp, - offerDenom: offerDenoms[i], - offerAmount: parseInt(offerAmounts[i]), - offerVolume: parseInt(offerAmounts[i]), askDenom: askDenoms[i], - askVolume: parseInt(returnAmounts[i]), + commissionAmount: parseInt(commissionAmounts[i]), + offerAmount: parseInt(offerAmounts[i]), + offerDenom: offerDenoms[i], returnAmount: parseInt(returnAmounts[i]), + spreadAmount: parseInt(spreadAmounts[i]), taxAmount: parseInt(taxAmounts[i]), - commissionAmount: parseInt(commissionAmounts[i]), - spreadAmount: parseInt(spreadAmounts[i]) + timestamp, + txhash }); } return swapData; @@ -109,16 +106,16 @@ function extractMsgProvideLiquidity( const firstAsset = msg.provide_liquidity.assets[0]; const secAsset = msg.provide_liquidity.assets[1]; return { - txhash, - timestamp, firstTokenAmount: parseInt(firstAsset.amount), + firstTokenDenom: parseAssetInfoOnlyDenom(firstAsset.info), firstTokenLp: parseInt(firstAsset.amount), - firstTokenDenom: parseAssetInfo(firstAsset.info), + opType: "provide", secondTokenAmount: parseInt(secAsset.amount), + secondTokenDenom: parseAssetInfoOnlyDenom(secAsset.info), secondTokenLp: parseInt(secAsset.amount), - secondTokenDenom: parseAssetInfo(secAsset.info), + timestamp, txCreator, - opType: "provide" + txhash }; } return undefined; @@ -149,16 +146,16 @@ function extractMsgWithdrawLiquidity( // sanity check. only push data if can parse asset successfully if (assets.length !== 4) continue; withdrawData.push({ - txhash, - timestamp, firstTokenAmount: parseInt(assets[0]), - firstTokenLp: parseInt(assets[0]), firstTokenDenom: assets[1], + firstTokenLp: parseInt(assets[0]), + opType: "withdraw", secondTokenAmount: parseInt(assets[2]), - secondTokenLp: parseInt(assets[2]), secondTokenDenom: assets[3], - txCreator, - opType: "withdraw" + secondTokenLp: parseInt(assets[2]), + timestamp, + txhash, + txCreator }); } return withdrawData; diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index f5410e10..6f16cd86 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -13,20 +13,23 @@ export type AssetData = { }; export type BasicTxData = { - txhash: string; timestamp: string; + txhash: string; }; -export type SwapOperationData = BasicTxData & { - offerDenom: string; - offerAmount: number; - offerVolume: number; +export type SwapOperationData = { askDenom: string; - askVolume: number; - returnAmount: number; - taxAmount: number; commissionAmount: number; + offerAmount: number; + offerDenom: string; + returnAmount: number; spreadAmount: number; + taxAmount: number; +} & BasicTxData; + +export type TokenVolumeData = { + denom: string; + volume: number; }; export type PairInfoData = { @@ -52,16 +55,16 @@ export type AccountTx = { export type LiquidityOpType = "provide" | "withdraw"; -export type ProvideLiquidityOperationData = BasicTxData & { +export type ProvideLiquidityOperationData = { firstTokenAmount: number; - firstTokenLp: number; firstTokenDenom: string; + firstTokenLp: number; secondTokenAmount: number; - secondTokenLp: number; secondTokenDenom: string; - txCreator: string; + secondTokenLp: number; opType: LiquidityOpType; -}; + txCreator: string; +} & BasicTxData; export type WithdrawLiquidityOperationData = ProvideLiquidityOperationData; diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index 3ac54a47..376646b9 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -8,39 +8,34 @@ describe("test-duckdb", () => { await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); }); - it("test-duckdb-queryLatestTimestampSwapOps-should-return-the-latest-timestamp-row", async () => { + it("test-duckdb-queryAllVolume-should-return-total-volume-of-hello", async () => { await duckDb.insertSwapOps([ { - txhash: "foo", - timestamp: new Date(1689610068000).toISOString(), - offerAmount: 1, - offerDenom: "0", - offerVolume: 1, askDenom: "orai", - askVolume: 2, + commissionAmount: 0, + offerAmount: 10000, + offerDenom: "hello", returnAmount: 0, + spreadAmount: 0, taxAmount: 0, - commissionAmount: 0, - spreadAmount: 0 + timestamp: new Date(1689610068000).toISOString(), + txhash: "foo" }, { - txhash: "foo", - timestamp: new Date(1589610068000).toISOString(), - offerAmount: 1, - offerDenom: "0", - offerVolume: 1, - askDenom: "orai", - askVolume: 2, - returnAmount: 0, - taxAmount: 0, + askDenom: "hello", commissionAmount: 0, - spreadAmount: 0 + offerAmount: 0, + offerDenom: "foo", + returnAmount: 1, + spreadAmount: 0, + taxAmount: 0, + timestamp: new Date(1589610068000).toISOString(), + txhash: "foo" } ]); - const queryResult = await duckDb.queryLatestTimestampSwapOps(); - expect(new Date(queryResult[0].timestamp).toISOString()).toEqual( - new Date("2023-07-17T16:07:48.000Z").toISOString() - ); + const queryResult = await duckDb.queryAllVolume("hello"); + expect(queryResult.volume).toEqual(10001); + expect(queryResult.denom).toEqual("hello"); }); it("test-duckdb-insert-bulk-should-throw-error-when-wrong-data", async () => { @@ -65,23 +60,23 @@ describe("test-duckdb", () => { it("test-duckdb-insert-bulk-should-pass-and-can-query", async () => { // act & test - const newDate = new Date().toISOString(); + const newDate = new Date(1689610068000).toISOString(); await duckDb.insertLpOps([ { - txhash: "foo", - timestamp: newDate, firstTokenAmount: 1, - firstTokenLp: 0, firstTokenDenom: "orai", + firstTokenLp: 0, + opType: "withdraw", secondTokenAmount: 2, - secondTokenLp: 0, secondTokenDenom: "atom", + secondTokenLp: 0, + timestamp: newDate, txCreator: "foobar", - opType: "withdraw" + txhash: "foo" } ]); - const queryResult = await duckDb.queryLpOps(); - console.log("query result: ", queryResult); + let queryResult = await duckDb.queryLpOps(); + queryResult[0].timestamp = new Date(queryResult[0].timestamp).toISOString(); expect(queryResult[0]).toEqual({ txhash: "foo", timestamp: newDate, From 9ecc3197a7ce7bdc17aa77a17585f66b36219ca5 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 18 Jul 2023 15:27:37 +0700 Subject: [PATCH 49/75] finished query all volumes --- packages/oraidex-server/src/index.ts | 9 ++- packages/oraidex-sync/src/db.ts | 31 ++++++---- packages/oraidex-sync/src/index.ts | 11 ++-- packages/oraidex-sync/tests/db.spec.ts | 78 +++++++++++++++----------- 4 files changed, 78 insertions(+), 51 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index ce26d5ff..a7ce81ce 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -16,7 +16,8 @@ import { PairInfoData, findPairAddress, simulateSwapPriceWithUsdt, - findUsdOraiInPair + findUsdOraiInPair, + toDisplay } from "@oraichain/oraidex-sync"; import cors from "cors"; import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; @@ -94,13 +95,15 @@ app.get("/tickers", async (req, res) => { parseAssetInfoOnlyDenom(info) === usdtCw20Address || parseAssetInfoOnlyDenom(info) === usdcCw20Address ); const { baseIndex, targetIndex, target } = findUsdOraiInPair(pair.asset_infos); + const baseVolume = await duckDb.queryAllVolume(parseAssetInfoOnlyDenom(pair.asset_infos[baseIndex])); + const targetVolume = await duckDb.queryAllVolume(parseAssetInfoOnlyDenom(pair.asset_infos[targetIndex])); let tickerInfo: TickerInfo = { ticker_id: tickerId, base_currency: symbols[baseIndex], target_currency: symbols[targetIndex], last_price: "", - base_volume: getRandomNumber(1000, 9999), // TODO: remove random - target_volume: getRandomNumber(1000, 9999), + base_volume: toDisplay(BigInt(baseVolume.volume)).toString(), // TODO: remove random + target_volume: toDisplay(BigInt(targetVolume.volume)).toString(), pool_id: pairAddr ?? "", base: symbols[baseIndex], target: symbols[targetIndex] diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 0558773f..b6a47de5 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -112,16 +112,25 @@ export class DuckDb { } async queryAllVolume(denom: string): Promise { - // TODO: justify limit 1 here - const offerVolume = await this.conn.all( - `SELECT offerDenom as denom, sum(offerAmount) as volume from swap_ops_data where denom = '${denom}' group by denom limit 1;` - ); - const askVolume = await this.conn.all( - `SELECT askDenom as denom, sum(returnAmount) as volume from swap_ops_data where denom = '${denom}' group by denom limit 1;` - ); - if (offerVolume.length === 0 && askVolume.length === 0) return { denom, volume: 0 }; - if (offerVolume.length === 0) return askVolume[0] as TokenVolumeData; - return offerVolume[0] as TokenVolumeData; + const volume = ( + await Promise.all([ + this.conn.all("SELECT sum(offerAmount) as volume from swap_ops_data where offerDenom = ?;", denom), + this.conn.all("SELECT sum(returnAmount) as volume from swap_ops_data where askDenom = ?;", denom) + ]) + ) + .flat() + .filter((vol) => vol.volume); + console.log("volume: ", volume); + return { + denom, + volume: volume.reduce((accumulator, currentObject) => { + return accumulator + currentObject.volume; + }, 0) + }; + } + + async querySwapOps() { + return this.conn.all("SELECT count(*) from swap_ops_data"); } async queryLpOps() { @@ -134,3 +143,5 @@ export class DuckDb { ); } } + +[{ volume: 1 }, { volume: 3 }]; diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 5160b693..ca5ed490 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -74,8 +74,7 @@ class WriteOrders extends WriteData { ); this.firstWrite = false; } - const { txs, offset: newOffset, queryTags } = chunk as Txs; - console.log("new offset: ", newOffset); + const { txs, offset: newOffset } = chunk as Txs; let result = parseTxs(txs); // collect the latest offer & ask volume to accumulate the results @@ -84,7 +83,11 @@ class WriteOrders extends WriteData { await this.insertParsedTxs(result); const lpOps = await this.queryLpOps(); - console.log("lp ops: ", lpOps); + const swapOpsCount = await this.duckDb.querySwapOps(); + console.log("lp ops: ", lpOps.length); + console.log("swap ops: ", swapOpsCount); + const totalVolume = await this.duckDb.queryAllVolume("orai"); + console.log("total volume: ", totalVolume); } catch (error) { console.log("error processing data: ", error); return false; @@ -184,7 +187,7 @@ class OraiDexSync { rpcUrl: this.rpcUrl, queryTags: [], limit: 100, - maxThreadLevel: 1, + maxThreadLevel: 3, interval: 5000 }).pipe(new WriteOrders(this.duckDb, initialData)); } catch (error) { diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index 376646b9..201f0d72 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -2,43 +2,50 @@ import { DuckDb } from "../src/db"; describe("test-duckdb", () => { let duckDb: DuckDb; - beforeAll(async () => { - // fixture - duckDb = await DuckDb.create(":memory:"); - await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); - }); - it("test-duckdb-queryAllVolume-should-return-total-volume-of-hello", async () => { - await duckDb.insertSwapOps([ - { - askDenom: "orai", - commissionAmount: 0, - offerAmount: 10000, - offerDenom: "hello", - returnAmount: 0, - spreadAmount: 0, - taxAmount: 0, - timestamp: new Date(1689610068000).toISOString(), - txhash: "foo" - }, - { - askDenom: "hello", - commissionAmount: 0, - offerAmount: 0, - offerDenom: "foo", - returnAmount: 1, - spreadAmount: 0, - taxAmount: 0, - timestamp: new Date(1589610068000).toISOString(), - txhash: "foo" - } - ]); - const queryResult = await duckDb.queryAllVolume("hello"); - expect(queryResult.volume).toEqual(10001); - expect(queryResult.denom).toEqual("hello"); - }); + it.each<[string, number]>([ + ["hello", 10001], + ["orai", 100], + ["foo", 10] + ])( + "test-duckdb-queryAllVolume-should-return-correct-total-volume-given-%s-should-have-%d", + async (denom, expectedVolume) => { + duckDb = await DuckDb.create(":memory:"); + await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); + await duckDb.insertSwapOps([ + { + askDenom: "orai", + commissionAmount: 0, + offerAmount: 10000, + offerDenom: "hello", + returnAmount: 100, + spreadAmount: 0, + taxAmount: 0, + timestamp: new Date(1689610068000).toISOString(), + txhash: "foo" + }, + { + askDenom: "hello", + commissionAmount: 0, + offerAmount: 10, + offerDenom: "foo", + returnAmount: 1, + spreadAmount: 0, + taxAmount: 0, + timestamp: new Date(1589610068000).toISOString(), + txhash: "foo" + } + ]); + const queryResult = await duckDb.queryAllVolume(denom); + expect(queryResult.volume).toEqual(expectedVolume); + expect(queryResult.denom).toEqual(denom); + } + ); it("test-duckdb-insert-bulk-should-throw-error-when-wrong-data", async () => { + //setup + duckDb = await DuckDb.create(":memory:"); + await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); // act & test await expect( duckDb.insertLpOps([ @@ -59,6 +66,9 @@ describe("test-duckdb", () => { }); it("test-duckdb-insert-bulk-should-pass-and-can-query", async () => { + //setup + duckDb = await DuckDb.create(":memory:"); + await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); // act & test const newDate = new Date(1689610068000).toISOString(); await duckDb.insertLpOps([ From 8c3cb45e19028c663e2d223512c5c6fde4dde657 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 18 Jul 2023 16:42:26 +0700 Subject: [PATCH 50/75] finished querying volume of a token --- packages/oraidex-server/src/helper.ts | 6 ++ packages/oraidex-server/src/index.ts | 87 +++++--------------- packages/oraidex-server/tests/helper.spec.ts | 12 +++ packages/oraidex-sync/src/db.ts | 37 +++++++-- packages/oraidex-sync/src/index.ts | 14 ++-- packages/oraidex-sync/tests/db.spec.ts | 46 +++++++++++ 6 files changed, 124 insertions(+), 78 deletions(-) create mode 100644 packages/oraidex-server/tests/helper.spec.ts diff --git a/packages/oraidex-server/src/helper.ts b/packages/oraidex-server/src/helper.ts index 3bb568ef..44784e58 100644 --- a/packages/oraidex-server/src/helper.ts +++ b/packages/oraidex-server/src/helper.ts @@ -1,3 +1,9 @@ export function parseSymbolsToTickerId(symbols: [string, string]) { return `${symbols[0]}_${symbols[1]}`; } + +export function getDate24hBeforeNow(time: Date) { + const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + const date24hBeforeNow = new Date(time.getTime() - twentyFourHoursInMilliseconds); + return date24hBeforeNow; +} diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index a7ce81ce..3843ddb5 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -1,62 +1,32 @@ import * as dotenv from "dotenv"; import express from "express"; import { - AssetData, DuckDb, - OraiDexSync, - PairMapping, TickerInfo, pairs, - simulateSwapPricePair, parseAssetInfoOnlyDenom, - usdtCw20Address, - usdcCw20Address, - getAllPairInfos, - parseAssetInfo, - PairInfoData, findPairAddress, simulateSwapPriceWithUsdt, findUsdOraiInPair, - toDisplay + toDisplay, + OraiDexSync } from "@oraichain/oraidex-sync"; import cors from "cors"; import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; -import { - AssetInfo, - OraiswapFactoryQueryClient, - OraiswapRouterQueryClient, - PairInfo -} from "@oraichain/oraidex-contracts-sdk"; -import { parseSymbolsToTickerId } from "./helper"; +import { OraiswapRouterQueryClient } from "@oraichain/oraidex-contracts-sdk"; +import { getDate24hBeforeNow, parseSymbolsToTickerId } from "./helper"; dotenv.config(); const app = express(); app.use(cors()); -function getRandomNumber(min: number, max: number): string { - return (Math.floor(Math.random() * (max - min + 1)) + min).toString(); -} - let duckDb: DuckDb; const port = parseInt(process.env.PORT) || 2024; const hostname = process.env.HOSTNAME || "0.0.0.0"; const rpcUrl = process.env.RPC_URL || "https://rpc.orai.io"; -async function queryAllPairInfos(): Promise { - const cosmwasmClient = await CosmWasmClient.connect(rpcUrl); - const firstFactoryClient = new OraiswapFactoryQueryClient( - cosmwasmClient, - process.env.FACTORY_CONTACT_ADDRESS_V1 || "orai1hemdkz4xx9kukgrunxu3yw0nvpyxf34v82d2c8" - ); - const secondFactoryClient = new OraiswapFactoryQueryClient( - cosmwasmClient, - process.env.FACTORY_CONTACT_ADDRESS_V2 || "orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" - ); - return getAllPairInfos(firstFactoryClient, secondFactoryClient); -} - app.get("/pairs", async (req, res) => { try { const pairInfos = await duckDb.queryPairInfos(); @@ -90,13 +60,20 @@ app.get("/tickers", async (req, res) => { const symbols = pair.symbols; const pairAddr = findPairAddress(pairInfos, pair.asset_infos); const tickerId = parseSymbolsToTickerId(symbols); - const hasUsdInPair = pair.asset_infos.some( - (info) => - parseAssetInfoOnlyDenom(info) === usdtCw20Address || parseAssetInfoOnlyDenom(info) === usdcCw20Address - ); const { baseIndex, targetIndex, target } = findUsdOraiInPair(pair.asset_infos); - const baseVolume = await duckDb.queryAllVolume(parseAssetInfoOnlyDenom(pair.asset_infos[baseIndex])); - const targetVolume = await duckDb.queryAllVolume(parseAssetInfoOnlyDenom(pair.asset_infos[targetIndex])); + const latestTimestamp = await duckDb.queryLatestTimestampSwapOps(); + const now = new Date(latestTimestamp); + const then = getDate24hBeforeNow(now).toISOString(); + const baseVolume = await duckDb.queryAllVolumeRange( + parseAssetInfoOnlyDenom(pair.asset_infos[baseIndex]), + then, + now.toISOString() + ); + const targetVolume = await duckDb.queryAllVolumeRange( + parseAssetInfoOnlyDenom(pair.asset_infos[targetIndex]), + then, + now.toISOString() + ); let tickerInfo: TickerInfo = { ticker_id: tickerId, base_currency: symbols[baseIndex], @@ -120,41 +97,21 @@ app.get("/tickers", async (req, res) => { ) ).map((result) => { if (result.status === "fulfilled") return result.value; + else console.log("result: ", result.reason); }); console.table(data); res.status(200).send(data); } catch (error) { + console.log("error: ", error); res.status(500).send(`Error: ${JSON.stringify(error)}`); } }); app.listen(port, hostname, async () => { // sync data for the service to read - duckDb = await DuckDb.create("oraidex-sync-data"); - await Promise.all([ - duckDb.createHeightSnapshot(), - duckDb.createLiquidityOpsTable(), - duckDb.createSwapOpsTable(), - duckDb.createPairInfosTable(), - duckDb.createPriceInfoTable() - ]); - const pairInfos = await queryAllPairInfos(); - // Promise.all([insert pool info, and insert pair info. Promise all because pool info & updated pair info must go together]) - await duckDb.insertPairInfos( - pairInfos.map( - (pair) => - ({ - firstAssetInfo: parseAssetInfo(pair.asset_infos[0]), - secondAssetInfo: parseAssetInfo(pair.asset_infos[1]), - commissionRate: pair.commission_rate, - pairAddr: pair.contract_addr, - liquidityAddr: pair.liquidity_token, - oracleAddr: pair.oracle_addr - } as PairInfoData) - ) - ); // console.dir(pairInfos, { depth: null }); - // const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); - // await oraidexSync.sync(); + duckDb = await DuckDb.create("oraidex-sync-data"); + const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); + await oraidexSync.sync(); console.log(`[server]: oraiDEX info server is running at http://${hostname}:${port}`); }); diff --git a/packages/oraidex-server/tests/helper.spec.ts b/packages/oraidex-server/tests/helper.spec.ts new file mode 100644 index 00000000..4c3d3fb2 --- /dev/null +++ b/packages/oraidex-server/tests/helper.spec.ts @@ -0,0 +1,12 @@ +import { getDate24hBeforeNow } from "../src/helper"; + +describe("test-helper", () => { + it("test-getDate24hBeforeNow", () => { + // setup + const now = new Date("2023-07-16T16:07:48.000Z"); + // act + const result = getDate24hBeforeNow(now); + // assert + expect(result).toEqual(new Date("2023-07-15T16:07:48.000Z")); + }); +}); diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index b6a47de5..67c70265 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -96,11 +96,6 @@ export class DuckDb { await this.insertBulkData(ops, "pair_infos", true); } - // we need to: - // price history should contain: timestamp, tx height, asset info, price - // if cannot find then we spawn another stream and sync it started from the common sync height. We will re-sync it if its latest height is too behind compared to the common sync height - // if - async createPriceInfoTable() { await this.conn.exec( "CREATE TABLE IF NOT EXISTS price_infos (txheight UINTEGER, timestamp TIMESTAMP, assetInfo VARCHAR, price UINTEGER)" @@ -129,6 +124,38 @@ export class DuckDb { }; } + async queryAllVolumeRange(denom: string, startTime: string, endTime: string) { + const volume = ( + await Promise.all([ + this.conn.all( + `SELECT sum(offerAmount) + as volume + from swap_ops_data + where offerDenom = ? and timestamp >= '${startTime}'::TIMESTAMP and timestamp <= '${endTime}'::TIMESTAMP`, + denom + ), + this.conn.all( + `SELECT sum(returnAmount) + as volume + from swap_ops_data + where askDenom = ? and timestamp >= '${startTime}'::TIMESTAMP and timestamp <= '${endTime}'::TIMESTAMP`, + denom + ) + ]) + ).flat(); + return { + denom, + volume: volume.reduce((accumulator, currentObject) => { + return accumulator + currentObject.volume; + }, 0) + }; + } + + async queryLatestTimestampSwapOps(): Promise { + const latestTimestamp = await this.conn.all("SELECT timestamp from swap_ops_data order by timestamp desc limit 1"); + return latestTimestamp[0].timestamp as string; + } + async querySwapOps() { return this.conn.all("SELECT count(*) from swap_ops_data"); } diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index ca5ed490..942d0d98 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -86,8 +86,6 @@ class WriteOrders extends WriteData { const swapOpsCount = await this.duckDb.querySwapOps(); console.log("lp ops: ", lpOps.length); console.log("swap ops: ", swapOpsCount); - const totalVolume = await this.duckDb.queryAllVolume("orai"); - console.log("total volume: ", totalVolume); } catch (error) { console.log("error processing data: ", error); return false; @@ -196,13 +194,13 @@ class OraiDexSync { } } -const start = async () => { - const duckDb = await DuckDb.create("oraidex-sync-data"); - const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); - await oraidexSync.sync(); -}; +// const start = async () => { +// const duckDb = await DuckDb.create("oraidex-sync-data"); +// const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); +// await oraidexSync.sync(); +// }; -start(); +// start(); export { OraiDexSync }; diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index 201f0d72..374c717f 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -42,6 +42,52 @@ describe("test-duckdb", () => { } ); + it("test-query-volume-last-24h", async () => { + duckDb = await DuckDb.create(":memory:"); + await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); + await duckDb.insertSwapOps([ + { + askDenom: "orai", + commissionAmount: 0, + offerAmount: 10, + offerDenom: "hello", + returnAmount: 100, + spreadAmount: 0, + taxAmount: 0, + timestamp: new Date("2023-07-17T16:07:48.000Z").toISOString(), + txhash: "foo" + }, + { + askDenom: "", + commissionAmount: 0, + offerAmount: 10, + offerDenom: "hello", + returnAmount: 1, + spreadAmount: 0, + taxAmount: 0, + timestamp: new Date("2023-07-16T16:07:48.000Z").toISOString(), + txhash: "foo" + }, + { + askDenom: "", + commissionAmount: 0, + offerAmount: 10, + offerDenom: "hello", + returnAmount: 1, + spreadAmount: 0, + taxAmount: 0, + timestamp: new Date("2023-07-16T15:07:48.000Z").toISOString(), + txhash: "foo" + } + ]); + const queryResult = await duckDb.queryAllVolumeRange( + "hello", + "2023-07-16T16:07:48.000Z", + "2023-07-17T16:07:48.000Z" + ); + expect(queryResult.volume).toEqual(20); + }); + it("test-duckdb-insert-bulk-should-throw-error-when-wrong-data", async () => { //setup duckDb = await DuckDb.create(":memory:"); From 7eb5912ae08fa63888d8639662a485bd063eea7f Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 18 Jul 2023 16:47:04 +0700 Subject: [PATCH 51/75] added env for limit & max thread level --- packages/oraidex-sync/.env.example | 4 +++- packages/oraidex-sync/src/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/oraidex-sync/.env.example b/packages/oraidex-sync/.env.example index 26e932c4..3bae3266 100644 --- a/packages/oraidex-sync/.env.example +++ b/packages/oraidex-sync/.env.example @@ -2,4 +2,6 @@ RPC_URL=https://rpc.orai.io FACTORY_CONTACT_ADDRESS_V1="orai1hemdkz4xx9kukgrunxu3yw0nvpyxf34v82d2c8" FACTORY_CONTACT_ADDRESS_V2="orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjschwugqjsqhst" ROUTER_CONTRACT_ADDRESS="orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" -MULTICALL_CONTRACT_ADDRESS="orai1q7x644gmf7h8u8y6y8t9z9nnwl8djkmspypr6mxavsk9ual7dj0sxpmgwd" \ No newline at end of file +MULTICALL_CONTRACT_ADDRESS="orai1q7x644gmf7h8u8y6y8t9z9nnwl8djkmspypr6mxavsk9ual7dj0sxpmgwd" +LIMIT=1000 +MAX_THREAD_LEVEL=3 \ No newline at end of file diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 942d0d98..c287f4ae 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -184,8 +184,8 @@ class OraiDexSync { offset: currentInd, rpcUrl: this.rpcUrl, queryTags: [], - limit: 100, - maxThreadLevel: 3, + limit: parseInt(process.env.LIMIT) || 100, + maxThreadLevel: parseInt(process.env.MAX_THREAD_LEVEL) || 3, interval: 5000 }).pipe(new WriteOrders(this.duckDb, initialData)); } catch (error) { From 73ff8efc672a07dc92645155a16318bdacaeb62f Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 18 Jul 2023 16:48:39 +0700 Subject: [PATCH 52/75] added log new offset --- packages/oraidex-sync/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index c287f4ae..94a0070f 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -79,6 +79,7 @@ class WriteOrders extends WriteData { // collect the latest offer & ask volume to accumulate the results // insert txs + console.log("new offset: ", newOffset); await this.duckDb.insertHeightSnapshot(newOffset); await this.insertParsedTxs(result); From eabd5a4172176ba1c576d6ae9554802290e8acd5 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 18 Jul 2023 16:54:28 +0700 Subject: [PATCH 53/75] commented out un-used parts --- packages/oraidex-sync/src/index.ts | 62 +++++++++++++++--------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 94a0070f..1ef231af 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -56,24 +56,24 @@ class WriteOrders extends WriteData { async process(chunk: any): Promise { try { - // first time calling of the application then we query past data and be ready to store them into the db for prefix sum - // this helps the flow go smoothly and remove dependency between different streams - if (this.firstWrite) { - console.log("initial data: ", this.initialData); - const { height, time } = this.initialData.blockHeader; - await this.duckDb.insertPriceInfos( - this.initialData.tokenPrices.map( - (tokenPrice) => - ({ - txheight: height, - timestamp: time, - assetInfo: parseAssetInfo(tokenPrice.info), - price: parseInt(tokenPrice.amount) - } as PriceInfo) - ) - ); - this.firstWrite = false; - } + // // first time calling of the application then we query past data and be ready to store them into the db for prefix sum + // // this helps the flow go smoothly and remove dependency between different streams + // if (this.firstWrite) { + // console.log("initial data: ", this.initialData); + // const { height, time } = this.initialData.blockHeader; + // await this.duckDb.insertPriceInfos( + // this.initialData.tokenPrices.map( + // (tokenPrice) => + // ({ + // txheight: height, + // timestamp: time, + // assetInfo: parseAssetInfo(tokenPrice.info), + // price: parseInt(tokenPrice.amount) + // } as PriceInfo) + // ) + // ); + // this.firstWrite = false; + // } const { txs, offset: newOffset } = chunk as Txs; let result = parseTxs(txs); @@ -168,19 +168,19 @@ class OraiDexSync { ]); let currentInd = await this.duckDb.loadHeightSnapshot(); let initialData: InitialData = { tokenPrices: [], blockHeader: undefined }; - console.log("current ind: ", currentInd); - // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic - if (currentInd <= 12388825) { - currentInd = 12388825; - } - - const tokenPrices = await Promise.all( - extractUniqueAndFlatten(pairs).map((info) => this.simulateSwapPrice(info, currentInd)) - ); - const initialBlockHeader = (await this.cosmwasmClient.getBlock(currentInd)).header; - initialData.tokenPrices = tokenPrices; - initialData.blockHeader = initialBlockHeader; - await this.updateLatestPairInfos(); + // console.log("current ind: ", currentInd); + // // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic + // if (currentInd <= 12388825) { + // currentInd = 12388825; + // } + + // const tokenPrices = await Promise.all( + // extractUniqueAndFlatten(pairs).map((info) => this.simulateSwapPrice(info, currentInd)) + // ); + // const initialBlockHeader = (await this.cosmwasmClient.getBlock(currentInd)).header; + // initialData.tokenPrices = tokenPrices; + // initialData.blockHeader = initialBlockHeader; + // await this.updateLatestPairInfos(); new SyncData({ offset: currentInd, rpcUrl: this.rpcUrl, From 74660c60a25221470c1c330d84a286a37c33b1da Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 18 Jul 2023 17:01:30 +0700 Subject: [PATCH 54/75] added guard check swap ops & remove await sync() in server --- packages/oraidex-server/src/index.ts | 2 +- packages/oraidex-sync/src/tx-parsing.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 3843ddb5..0c02cfd2 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -112,6 +112,6 @@ app.listen(port, hostname, async () => { // console.dir(pairInfos, { depth: null }); duckDb = await DuckDb.create("oraidex-sync-data"); const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); - await oraidexSync.sync(); + oraidexSync.sync(); console.log(`[server]: oraiDEX info server is running at http://${hostname}:${port}`); }); diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 364ce840..93a4b478 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -80,6 +80,9 @@ function extractSwapOperations(txhash: string, timestamp: string, events: readon } } } + const swapAttrs = [offerAmounts, offerDenoms, askDenoms, returnAmounts, taxAmounts, commissionAmounts, spreadAmounts]; + // faulty swap attributes, wont collect + if (!swapAttrs.every((array) => array.length === askDenoms.length)) return []; for (let i = 0; i < askDenoms.length; i++) { swapData.push({ askDenom: askDenoms[i], From c1ddb5799062791ac861239231613bb45fe69692 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 18 Jul 2023 18:42:09 +0700 Subject: [PATCH 55/75] finished query volume based on pair --- packages/oraidex-server/src/index.ts | 19 ++--- packages/oraidex-sync/src/db.ts | 103 +++++++++++++++++------- packages/oraidex-sync/src/helper.ts | 16 ++++ packages/oraidex-sync/src/types.ts | 9 ++- packages/oraidex-sync/tests/db.spec.ts | 105 ++++++++++++++++++++----- 5 files changed, 190 insertions(+), 62 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 0c02cfd2..40d739d1 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -64,23 +64,16 @@ app.get("/tickers", async (req, res) => { const latestTimestamp = await duckDb.queryLatestTimestampSwapOps(); const now = new Date(latestTimestamp); const then = getDate24hBeforeNow(now).toISOString(); - const baseVolume = await duckDb.queryAllVolumeRange( - parseAssetInfoOnlyDenom(pair.asset_infos[baseIndex]), - then, - now.toISOString() - ); - const targetVolume = await duckDb.queryAllVolumeRange( - parseAssetInfoOnlyDenom(pair.asset_infos[targetIndex]), - then, - now.toISOString() - ); + const baseInfo = parseAssetInfoOnlyDenom(pair.asset_infos[baseIndex]); + const targetInfo = parseAssetInfoOnlyDenom(pair.asset_infos[targetIndex]); + const volume = await duckDb.queryAllVolumeRange(baseInfo, targetInfo, then, now.toISOString()); let tickerInfo: TickerInfo = { ticker_id: tickerId, base_currency: symbols[baseIndex], target_currency: symbols[targetIndex], last_price: "", - base_volume: toDisplay(BigInt(baseVolume.volume)).toString(), // TODO: remove random - target_volume: toDisplay(BigInt(targetVolume.volume)).toString(), + base_volume: toDisplay(BigInt(volume.volume[baseInfo])).toString(), // TODO: remove random + target_volume: toDisplay(BigInt(volume.volume[targetInfo])).toString(), pool_id: pairAddr ?? "", base: symbols[baseIndex], target: symbols[targetIndex] @@ -112,6 +105,6 @@ app.listen(port, hostname, async () => { // console.dir(pairInfos, { depth: null }); duckDb = await DuckDb.create("oraidex-sync-data"); const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); - oraidexSync.sync(); + // oraidexSync.sync(); console.log(`[server]: oraiDEX info server is running at http://${hostname}:${port}`); }); diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 67c70265..87987101 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -1,6 +1,14 @@ import { Database, Connection } from "duckdb-async"; -import { PairInfoData, PriceInfo, SwapOperationData, TokenVolumeData, WithdrawLiquidityOperationData } from "./types"; -import fs from "fs"; +import { + PairInfoData, + PriceInfo, + SwapOperationData, + TokenVolumeData, + VolumeData, + WithdrawLiquidityOperationData +} from "./types"; +import fs, { rename } from "fs"; +import { renameKey, replaceAllNonAlphaBetChar } from "./helper"; export class DuckDb { protected constructor(public readonly conn: Connection) {} @@ -106,48 +114,89 @@ export class DuckDb { await this.insertBulkData(ops, "price_infos", false, `price_infos-${Math.random() * 1000}`); } - async queryAllVolume(denom: string): Promise { + reduceVolume( + volume: VolumeData[], + data: { modifiedOfferDenom: string; modifiedAskDenom: string; offerDenom: string; askDenom: string } + ): VolumeData { + // by default, the offer denom & ask denom + const { offerDenom, modifiedOfferDenom, askDenom, modifiedAskDenom } = data; + let volumeData = volume.reduce((accumulator, currentObject) => { + accumulator[modifiedOfferDenom] = (accumulator[modifiedOfferDenom] || 0) + currentObject[modifiedOfferDenom]; + accumulator[modifiedAskDenom] = (accumulator[modifiedAskDenom] || 0) + currentObject[modifiedAskDenom]; + return accumulator; + }, {}) as VolumeData; + volumeData = renameKey(volumeData, modifiedOfferDenom, offerDenom); + volumeData = renameKey(volumeData, modifiedAskDenom, askDenom); + + return volumeData; + } + + async queryAllVolume(offerDenom: string, askDenom: string): Promise { + const modifiedOfferDenom = replaceAllNonAlphaBetChar(offerDenom); + const modifiedAskDenom = replaceAllNonAlphaBetChar(askDenom); const volume = ( await Promise.all([ - this.conn.all("SELECT sum(offerAmount) as volume from swap_ops_data where offerDenom = ?;", denom), - this.conn.all("SELECT sum(returnAmount) as volume from swap_ops_data where askDenom = ?;", denom) + this.conn.all( + `SELECT sum(offerAmount) as ${modifiedOfferDenom}, sum(returnAmount) as ${modifiedAskDenom} + from swap_ops_data + where offerDenom = ? + and askDenom = ?`, + offerDenom, + askDenom + ), + this.conn.all( + `SELECT sum(offerAmount) as ${modifiedAskDenom}, sum(returnAmount) as ${modifiedOfferDenom} + from swap_ops_data + where offerDenom = ? + and askDenom = ?`, + askDenom, + offerDenom + ) ]) - ) - .flat() - .filter((vol) => vol.volume); - console.log("volume: ", volume); + ).flat(); return { - denom, - volume: volume.reduce((accumulator, currentObject) => { - return accumulator + currentObject.volume; - }, 0) + offerDenom, + askDenom, + volume: this.reduceVolume(volume, { offerDenom, modifiedOfferDenom, askDenom, modifiedAskDenom }) }; } - async queryAllVolumeRange(denom: string, startTime: string, endTime: string) { + async queryAllVolumeRange( + offerDenom: string, + askDenom: string, + startTime: string, + endTime: string + ): Promise { + const modifiedOfferDenom = replaceAllNonAlphaBetChar(offerDenom); + const modifiedAskDenom = replaceAllNonAlphaBetChar(askDenom); const volume = ( await Promise.all([ this.conn.all( - `SELECT sum(offerAmount) - as volume + `SELECT sum(offerAmount) as ${modifiedOfferDenom}, sum(returnAmount) as ${modifiedAskDenom} from swap_ops_data - where offerDenom = ? and timestamp >= '${startTime}'::TIMESTAMP and timestamp <= '${endTime}'::TIMESTAMP`, - denom + where offerDenom = ? + and askDenom = ? + and timestamp >= '${startTime}'::TIMESTAMP + and timestamp <= '${endTime}'::TIMESTAMP`, + offerDenom, + askDenom ), this.conn.all( - `SELECT sum(returnAmount) - as volume + `SELECT sum(offerAmount) as ${modifiedAskDenom}, sum(returnAmount) as ${modifiedOfferDenom} from swap_ops_data - where askDenom = ? and timestamp >= '${startTime}'::TIMESTAMP and timestamp <= '${endTime}'::TIMESTAMP`, - denom + where offerDenom = ? + and askDenom = ? + and timestamp >= '${startTime}'::TIMESTAMP + and timestamp <= '${endTime}'::TIMESTAMP`, + askDenom, + offerDenom ) ]) ).flat(); return { - denom, - volume: volume.reduce((accumulator, currentObject) => { - return accumulator + currentObject.volume; - }, 0) + offerDenom, + askDenom, + volume: this.reduceVolume(volume, { offerDenom, modifiedOfferDenom, askDenom, modifiedAskDenom }) }; } @@ -170,5 +219,3 @@ export class DuckDb { ); } } - -[{ volume: 1 }, { volume: 3 }]; diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index af4242ea..982c2913 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -45,6 +45,22 @@ export const toDisplay = (amount: string | bigint, sourceDecimals = 6, desDecima return Number(returnAmount) / (displayDecimals === truncDecimals ? atomic : 10 ** displayDecimals); }; +export function renameKey(object: Object, oldKey: string, newKey: string): any { + if (oldKey === newKey) return object; + // Check if the old key exists in the object + if (object.hasOwnProperty(oldKey)) { + // Create the new key and assign the value from the old key + object[newKey] = object[oldKey]; + // Delete the old key + delete object[oldKey]; + } + return object; +} + +export function replaceAllNonAlphaBetChar(columnName: string): string { + return columnName.replace(/[^a-zA-Z]/g, "a"); +} + function parseAssetInfo(info: AssetInfo): string { // if ("native_token" in info) return info.native_token.denom; // return info.token.contract_addr; diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 6f16cd86..9fabf9ab 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -27,9 +27,14 @@ export type SwapOperationData = { taxAmount: number; } & BasicTxData; +export type VolumeData = { + [k: string]: number; +}; + export type TokenVolumeData = { - denom: string; - volume: number; + offerDenom: string; + askDenom: string; + volume: VolumeData; }; export type PairInfoData = { diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index 374c717f..4695150d 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -1,12 +1,13 @@ import { DuckDb } from "../src/db"; +import { toDisplay } from "../src/helper"; describe("test-duckdb", () => { let duckDb: DuckDb; it.each<[string, number]>([ - ["hello", 10001], - ["orai", 100], - ["foo", 10] + ["hello", 10001] + // ["orai", 100], + // ["foo", 10] ])( "test-duckdb-queryAllVolume-should-return-correct-total-volume-given-%s-should-have-%d", async (denom, expectedVolume) => { @@ -17,7 +18,7 @@ describe("test-duckdb", () => { askDenom: "orai", commissionAmount: 0, offerAmount: 10000, - offerDenom: "hello", + offerDenom: "atom", returnAmount: 100, spreadAmount: 0, taxAmount: 0, @@ -25,10 +26,32 @@ describe("test-duckdb", () => { txhash: "foo" }, { - askDenom: "hello", + askDenom: "orai", + commissionAmount: 0, + offerAmount: 10, + offerDenom: "atom", + returnAmount: 1, + spreadAmount: 0, + taxAmount: 0, + timestamp: new Date(1589610068000).toISOString(), + txhash: "foo" + }, + { + askDenom: "atom", commissionAmount: 0, offerAmount: 10, - offerDenom: "foo", + offerDenom: "orai", + returnAmount: 1, + spreadAmount: 0, + taxAmount: 0, + timestamp: new Date(1589610068000).toISOString(), + txhash: "foo" + }, + { + askDenom: "atom", + commissionAmount: 0, + offerAmount: 10, + offerDenom: "orai", returnAmount: 1, spreadAmount: 0, taxAmount: 0, @@ -36,9 +59,15 @@ describe("test-duckdb", () => { txhash: "foo" } ]); - const queryResult = await duckDb.queryAllVolume(denom); - expect(queryResult.volume).toEqual(expectedVolume); - expect(queryResult.denom).toEqual(denom); + let queryResult = await duckDb.queryAllVolume("orai", "atom"); + console.log("query result: ", queryResult); + expect(queryResult.volume["orai"]).toEqual(121); + expect(queryResult.volume["atom"]).toEqual(10012); + + queryResult = await duckDb.queryAllVolume("atom", "orai"); + console.log("query result: ", queryResult); + expect(queryResult.volume["orai"]).toEqual(121); + expect(queryResult.volume["atom"]).toEqual(10012); } ); @@ -49,8 +78,8 @@ describe("test-duckdb", () => { { askDenom: "orai", commissionAmount: 0, - offerAmount: 10, - offerDenom: "hello", + offerAmount: 10000, + offerDenom: "atom", returnAmount: 100, spreadAmount: 0, taxAmount: 0, @@ -58,10 +87,10 @@ describe("test-duckdb", () => { txhash: "foo" }, { - askDenom: "", + askDenom: "atom", commissionAmount: 0, offerAmount: 10, - offerDenom: "hello", + offerDenom: "orai", returnAmount: 1, spreadAmount: 0, taxAmount: 0, @@ -69,23 +98,46 @@ describe("test-duckdb", () => { txhash: "foo" }, { - askDenom: "", + askDenom: "orai", commissionAmount: 0, offerAmount: 10, - offerDenom: "hello", + offerDenom: "atom", returnAmount: 1, spreadAmount: 0, taxAmount: 0, - timestamp: new Date("2023-07-16T15:07:48.000Z").toISOString(), + timestamp: new Date(1389610068000).toISOString(), + txhash: "foo" + }, + { + askDenom: "atom", + commissionAmount: 0, + offerAmount: 10, + offerDenom: "orai", + returnAmount: 1, + spreadAmount: 0, + taxAmount: 0, + timestamp: new Date(1389610068000).toISOString(), txhash: "foo" } ]); - const queryResult = await duckDb.queryAllVolumeRange( - "hello", + let queryResult = await duckDb.queryAllVolumeRange( + "orai", + "atom", + "2023-07-16T16:07:48.000Z", + "2023-07-17T16:07:48.000Z" + ); + console.log("result: ", queryResult); + expect(queryResult.volume["orai"]).toEqual(110); + expect(queryResult.volume["atom"]).toEqual(10001); + + queryResult = await duckDb.queryAllVolumeRange( + "orai", + "atom", "2023-07-16T16:07:48.000Z", "2023-07-17T16:07:48.000Z" ); - expect(queryResult.volume).toEqual(20); + expect(queryResult.volume["orai"]).toEqual(110); + expect(queryResult.volume["atom"]).toEqual(10001); }); it("test-duckdb-insert-bulk-should-throw-error-when-wrong-data", async () => { @@ -146,4 +198,19 @@ describe("test-duckdb", () => { opType: "withdraw" }); }); + + // it("test-realdb", async () => { + // duckDb = await DuckDb.create("oraidex-sync-data"); + // const latestTimestamp = await duckDb.queryLatestTimestampSwapOps(); + // const now = new Date(latestTimestamp); + // function getDate24hBeforeNow(time: Date) { + // const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + // const date24hBeforeNow = new Date(time.getTime() - twentyFourHoursInMilliseconds); + // return date24hBeforeNow; + // } + // const then = getDate24hBeforeNow(now).toISOString(); + // // const baseVolume = await duckDb.queryAllVolumeRange("orai", then, now.toISOString()); + // const baseVolume = await duckDb.queryAllVolume("orai"); + // console.log("base volume: ", toDisplay(BigInt(baseVolume.volume))); + // }); }); From 90c55f6e100d46b9af7a747cf39276ba93d287f3 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 18 Jul 2023 18:46:23 +0700 Subject: [PATCH 56/75] uncomment sync function --- packages/oraidex-server/src/index.ts | 2 +- packages/oraidex-sync/src/db.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 40d739d1..343a0029 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -105,6 +105,6 @@ app.listen(port, hostname, async () => { // console.dir(pairInfos, { depth: null }); duckDb = await DuckDb.create("oraidex-sync-data"); const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); - // oraidexSync.sync(); + oraidexSync.sync(); console.log(`[server]: oraiDEX info server is running at http://${hostname}:${port}`); }); diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 87987101..ed68a8cf 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -167,6 +167,7 @@ export class DuckDb { startTime: string, endTime: string ): Promise { + // need to replace because the denom can contain numbers and other figures. We replace for temporary only, will be reverted once finish reducing const modifiedOfferDenom = replaceAllNonAlphaBetChar(offerDenom); const modifiedAskDenom = replaceAllNonAlphaBetChar(askDenom); const volume = ( @@ -196,7 +197,7 @@ export class DuckDb { return { offerDenom, askDenom, - volume: this.reduceVolume(volume, { offerDenom, modifiedOfferDenom, askDenom, modifiedAskDenom }) + volume: this.reduceVolume(volume, { offerDenom, modifiedOfferDenom, askDenom, modifiedAskDenom }) // reduce volume to aggregate ask & offer volume of a token together }; } From d932170ca0338698c91491d848861032efd61b62 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 10:38:10 +0700 Subject: [PATCH 57/75] refactored & changed way to calc volume from usdt to pair --- packages/oraidex-server/src/index.ts | 13 ++- packages/oraidex-sync/src/db.ts | 2 +- packages/oraidex-sync/src/helper.ts | 56 ++++----- packages/oraidex-sync/src/index.ts | 5 +- packages/oraidex-sync/src/pairs.ts | 42 +++++-- packages/oraidex-sync/src/query.ts | 33 +++--- packages/oraidex-sync/tests/helper.spec.ts | 127 +++++++++++++++------ 7 files changed, 167 insertions(+), 111 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 343a0029..88435080 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -6,10 +6,9 @@ import { pairs, parseAssetInfoOnlyDenom, findPairAddress, - simulateSwapPriceWithUsdt, - findUsdOraiInPair, toDisplay, - OraiDexSync + OraiDexSync, + simulateSwapPrice } from "@oraichain/oraidex-sync"; import cors from "cors"; import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; @@ -60,7 +59,9 @@ app.get("/tickers", async (req, res) => { const symbols = pair.symbols; const pairAddr = findPairAddress(pairInfos, pair.asset_infos); const tickerId = parseSymbolsToTickerId(symbols); - const { baseIndex, targetIndex, target } = findUsdOraiInPair(pair.asset_infos); + // const { baseIndex, targetIndex, target } = findUsdOraiInPair(pair.asset_infos); + const baseIndex = 0; + const targetIndex = 1; const latestTimestamp = await duckDb.queryLatestTimestampSwapOps(); const now = new Date(latestTimestamp); const then = getDate24hBeforeNow(now).toISOString(); @@ -80,8 +81,8 @@ app.get("/tickers", async (req, res) => { }; try { // reverse because in pairs, we put base info as first index - const price = await simulateSwapPriceWithUsdt(target, routerContract); - tickerInfo.last_price = price.amount; + const price = await simulateSwapPrice(pair.asset_infos, routerContract); + tickerInfo.last_price = price; } catch (error) { tickerInfo.last_price = "0"; } diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index ed68a8cf..adeb26bc 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -118,7 +118,7 @@ export class DuckDb { volume: VolumeData[], data: { modifiedOfferDenom: string; modifiedAskDenom: string; offerDenom: string; askDenom: string } ): VolumeData { - // by default, the offer denom & ask denom + // by default, the offer denom & ask denom are in modified state. Need to rename them afterwards const { offerDenom, modifiedOfferDenom, askDenom, modifiedAskDenom } = data; let volumeData = volume.reduce((accumulator, currentObject) => { accumulator[modifiedOfferDenom] = (accumulator[modifiedOfferDenom] || 0) + currentObject[modifiedOfferDenom]; diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index 982c2913..c4c968d8 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -129,23 +129,6 @@ function generateSwapOperations(infoPath: AssetInfo[]): SwapOperation[] { return swapOps; } -function extractUniqueAndFlatten(data: PairMapping[]): AssetInfo[] { - const uniqueItems = new Set(); - - data.forEach((item) => { - item.asset_infos.forEach((info) => { - const stringValue = JSON.stringify(info); - - if (!uniqueItems.has(stringValue)) { - uniqueItems.add(stringValue); - } - }); - }); - - const uniqueFlattenedArray = Array.from(uniqueItems).map((item) => JSON.parse(item as string)); - - return uniqueFlattenedArray; -} function findPairAddress(pairInfos: PairInfoData[], infos: [AssetInfo, AssetInfo]) { return pairInfos.find( (pairInfo) => @@ -158,32 +141,35 @@ function calculatePriceByPool(offerPool: bigint, askPool: bigint, commissionRate return (askPool - (offerPool * askPool) / (offerPool + BigInt(tenAmountInDecimalSix))) * BigInt(1 - commissionRate); } -function findUsdOraiInPair(infos: [AssetInfo, AssetInfo]): { - baseIndex: number; - targetIndex: number; - target: AssetInfo; -} { - const firstInfo = parseAssetInfoOnlyDenom(infos[0]); - const secondInfo = parseAssetInfoOnlyDenom(infos[1]); - if (firstInfo === usdtCw20Address || firstInfo === usdcCw20Address) - return { baseIndex: 0, targetIndex: 1, target: infos[1] }; - if (secondInfo === usdtCw20Address || secondInfo === usdcCw20Address) - return { baseIndex: 1, targetIndex: 0, target: infos[0] }; - if (firstInfo === ORAI) return { baseIndex: 0, targetIndex: 1, target: infos[1] }; - if (secondInfo === ORAI) return { baseIndex: 1, targetIndex: 0, target: infos[0] }; - return { baseIndex: 1, targetIndex: 0, target: infos[0] }; // default we calculate the first info in the asset info list -} +// /** +// * +// * @param infos +// * @returns +// */ +// function findUsdOraiInPair(infos: [AssetInfo, AssetInfo]): { +// baseIndex: number; +// targetIndex: number; +// target: AssetInfo; +// } { +// const firstInfo = parseAssetInfoOnlyDenom(infos[0]); +// const secondInfo = parseAssetInfoOnlyDenom(infos[1]); +// if (firstInfo === usdtCw20Address || firstInfo === usdcCw20Address) +// return { baseIndex: 0, targetIndex: 1, target: infos[1] }; +// if (secondInfo === usdtCw20Address || secondInfo === usdcCw20Address) +// return { baseIndex: 1, targetIndex: 0, target: infos[0] }; +// if (firstInfo === ORAI) return { baseIndex: 0, targetIndex: 1, target: infos[1] }; +// if (secondInfo === ORAI) return { baseIndex: 1, targetIndex: 0, target: infos[0] }; +// return { baseIndex: 1, targetIndex: 0, target: infos[0] }; // default we calculate the first info in the asset info list +// } export { calculatePrefixSum, findMappedTargetedAssetInfo, findAssetInfoPathToUsdt, generateSwapOperations, - extractUniqueAndFlatten, parseAssetInfo, parseAssetInfoOnlyDenom, delay, findPairAddress, - calculatePriceByPool, - findUsdOraiInPair + calculatePriceByPool }; diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 1ef231af..67802de8 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -17,13 +17,10 @@ import { TxAnlysisResult, WithdrawLiquidityOperationData, InitialData, - PriceInfo, - PairInfoData, - PrefixSumHandlingData + PairInfoData } from "./types"; import { MulticallQueryClient } from "@oraichain/common-contracts-sdk"; import { PoolResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapPair.types"; -import { calculatePrefixSum, extractUniqueAndFlatten } from "./helper"; import { getAllPairInfos, getPoolInfos, simulateSwapPriceWithUsdt } from "./query"; class WriteOrders extends WriteData { diff --git a/packages/oraidex-sync/src/pairs.ts b/packages/oraidex-sync/src/pairs.ts index e6ae3844..b3ddd51d 100644 --- a/packages/oraidex-sync/src/pairs.ts +++ b/packages/oraidex-sync/src/pairs.ts @@ -1,5 +1,6 @@ // TODO: Need to somehow synchronize with the whitelist pairs on oraiDEX. Maybe it can be a smart contract containing all whitelisted pairs +import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; import { ORAI, airiCw20Adress, @@ -16,18 +17,19 @@ import { } from "./constants"; import { PairMapping } from "./types"; +// the orders are important! Do not change the order of the asset_infos. export const pairs: PairMapping[] = [ { - asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: airiCw20Adress } }], - symbols: ["ORAI", "AIRI"] + asset_infos: [{ token: { contract_addr: airiCw20Adress } }, { native_token: { denom: ORAI } }], + symbols: ["AIRI", "ORAI"] }, { - asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: oraixCw20Address } }], - symbols: ["ORAI", "ORAIX"] + asset_infos: [{ token: { contract_addr: oraixCw20Address } }, { native_token: { denom: ORAI } }], + symbols: ["ORAIX", "ORAI"] }, { - asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: scOraiCw20Address } }], - symbols: ["ORAI", "scORAI"] + asset_infos: [{ token: { contract_addr: scOraiCw20Address } }, { native_token: { denom: ORAI } }], + symbols: ["scORAI", "ORAI"] }, { asset_infos: [{ native_token: { denom: ORAI } }, { native_token: { denom: atomIbcDenom } }], @@ -38,8 +40,8 @@ export const pairs: PairMapping[] = [ symbols: ["ORAI", "USDT"] }, { - asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: kwtCw20Address } }], - symbols: ["ORAI", "KWT"] + asset_infos: [{ token: { contract_addr: kwtCw20Address } }, { native_token: { denom: ORAI } }], + symbols: ["KWT", "ORAI"] }, { asset_infos: [ @@ -63,7 +65,27 @@ export const pairs: PairMapping[] = [ symbols: ["ORAI", "WTRX"] }, { - asset_infos: [{ native_token: { denom: atomIbcDenom } }, { token: { contract_addr: scAtomCw20Address } }], - symbols: ["ATOM", "scATOM"] + asset_infos: [{ token: { contract_addr: scAtomCw20Address } }, { native_token: { denom: atomIbcDenom } }], + symbols: ["scATOM", "ATOM"] } ]; + +export function extractUniqueAndFlatten(data: PairMapping[]): AssetInfo[] { + const uniqueItems = new Set(); + + data.forEach((item) => { + item.asset_infos.forEach((info) => { + const stringValue = JSON.stringify(info); + + if (!uniqueItems.has(stringValue)) { + uniqueItems.add(stringValue); + } + }); + }); + + const uniqueFlattenedArray = Array.from(uniqueItems).map((item) => JSON.parse(item as string)); + + return uniqueFlattenedArray; +} + +export const uniqueInfos = extractUniqueAndFlatten(pairs); diff --git a/packages/oraidex-sync/src/query.ts b/packages/oraidex-sync/src/query.ts index e329f23a..3be3c8ff 100644 --- a/packages/oraidex-sync/src/query.ts +++ b/packages/oraidex-sync/src/query.ts @@ -31,6 +31,7 @@ async function getAllPairInfos( factoryV1: OraiswapFactoryReadOnlyInterface, factoryV2: OraiswapFactoryReadOnlyInterface ): Promise { + // TODO: change this to multicall const liquidityResults: PairInfo[] = ( await Promise.allSettled([ ...pairs.map((pair) => factoryV1.pair({ assetInfos: pair.asset_infos })), @@ -48,27 +49,19 @@ async function getAllPairInfos( async function simulateSwapPriceWithUsdt(info: AssetInfo, router: OraiswapRouterReadOnlyInterface): Promise { // adjust the query height to get data from the past const infoPath = findAssetInfoPathToUsdt(info); - // usdt case, price is always 1 - const operations = generateSwapOperations(infoPath); - if (operations.length === 0) return { info, amount: "0" }; // error case. Will be handled by the caller function - try { - const data = await router.simulateSwapOperations({ - offerAmount: tenAmountInDecimalSix, - operations - }); - return { info, amount: toDisplay(data.amount, 7).toString() }; // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit - } catch (error) { - console.log(`Error when trying to simulate swap with asset info: ${JSON.stringify(info)} using router: ${error}`); - return { info, amount: "0" }; // error case. Will be handled by the caller function - } + const amount = await simulateSwapPrice(infoPath, router); + return { info, amount }; } -async function simulateSwapPricePair( - pair: [AssetInfo, AssetInfo], - router: OraiswapRouterReadOnlyInterface -): Promise { +/** + * Simulate price for pair[0]/pair[pair.length - 1] where the amount of pair[0] is 10^7. This is a multihop simulate swap function. The asset infos in between of the array are for hopping + * @param pairPath - the path starting from the offer asset info to the ask asset info + * @param router - router contract + * @returns - pricea fter simulating + */ +async function simulateSwapPrice(pairPath: AssetInfo[], router: OraiswapRouterReadOnlyInterface): Promise { // usdt case, price is always 1 - const operations = generateSwapOperations(pair); + const operations = generateSwapOperations(pairPath); if (operations.length === 0) return "0"; // error case. Will be handled by the caller function try { const data = await router.simulateSwapOperations({ @@ -77,9 +70,9 @@ async function simulateSwapPricePair( }); return toDisplay(data.amount, 7).toString(); // since we simulate using 10 units, not 1. We use 10 because its a workaround for pools that are too small to simulate using 1 unit } catch (error) { - console.log(`Error when trying to simulate swap with pair: ${JSON.stringify(pair)} using router: ${error}`); + console.log(`Error when trying to simulate swap with pair: ${JSON.stringify(pairPath)} using router: ${error}`); return "0"; // error case. Will be handled by the caller function } } -export { getAllPairInfos, getPoolInfos, simulateSwapPriceWithUsdt, simulateSwapPricePair }; +export { getAllPairInfos, getPoolInfos, simulateSwapPriceWithUsdt, simulateSwapPrice }; diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index 15c5d134..4981f1a3 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -3,21 +3,23 @@ import { calculatePrefixSum, findAssetInfoPathToUsdt, findMappedTargetedAssetInfo, - extractUniqueAndFlatten, findPairAddress, calculatePriceByPool, - findUsdOraiInPair, toAmount, toDisplay, toDecimal } from "../src/helper"; -import { pairs } from "../src/pairs"; +import { extractUniqueAndFlatten, pairs } from "../src/pairs"; import { ORAI, airiCw20Adress, atomIbcDenom, + kwtCw20Address, milkyCw20Address, + oraixCw20Address, + osmosisIbcDenom, scAtomCw20Address, + scOraiCw20Address, tronCw20Address, usdcCw20Address, usdtCw20Address @@ -160,8 +162,8 @@ describe("test-helper", () => { const result = extractUniqueAndFlatten(pairs); // assert expect(result).toEqual([ - { native_token: { denom: "orai" } }, { token: { contract_addr: "orai10ldgzued6zjp0mkqwsv2mux3ml50l97c74x8sg" } }, + { native_token: { denom: "orai" } }, { token: { contract_addr: "orai1lus0f0rhx8s03gdllx2n6vhkmf0536dv57wfge" } }, { token: { contract_addr: "orai1065qe48g7aemju045aeyprflytemx7kecxkf5m7u5h5mphd0qlcs47pclp" } @@ -210,37 +212,92 @@ describe("test-helper", () => { expect(result).toEqual(expectedPairAddr); }); - it("test-calculatePriceByPool", () => { - const result = calculatePriceByPool(BigInt(10305560305234), BigInt(10205020305234), 0); - expect(result).toEqual(BigInt(9902432)); + it("test-pairs-should-persist-correct-order-and-has-correct-data", () => { + // this test should be updated once there's a new pair coming + expect(pairs).toEqual([ + { + asset_infos: [{ token: { contract_addr: airiCw20Adress } }, { native_token: { denom: ORAI } }], + symbols: ["AIRI", "ORAI"] + }, + { + asset_infos: [{ token: { contract_addr: oraixCw20Address } }, { native_token: { denom: ORAI } }], + symbols: ["ORAIX", "ORAI"] + }, + { + asset_infos: [{ token: { contract_addr: scOraiCw20Address } }, { native_token: { denom: ORAI } }], + symbols: ["scORAI", "ORAI"] + }, + { + asset_infos: [{ native_token: { denom: ORAI } }, { native_token: { denom: atomIbcDenom } }], + symbols: ["ORAI", "ATOM"] + }, + { + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdtCw20Address } }], + symbols: ["ORAI", "USDT"] + }, + { + asset_infos: [{ token: { contract_addr: kwtCw20Address } }, { native_token: { denom: ORAI } }], + symbols: ["KWT", "ORAI"] + }, + { + asset_infos: [ + { native_token: { denom: ORAI } }, + { + native_token: { denom: osmosisIbcDenom } + } + ], + symbols: ["ORAI", "OSMOSIS"] + }, + { + asset_infos: [{ token: { contract_addr: milkyCw20Address } }, { token: { contract_addr: usdtCw20Address } }], + symbols: ["MILKY", "USDT"] + }, + { + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdcCw20Address } }], + symbols: ["ORAI", "USDC"] + }, + { + asset_infos: [{ native_token: { denom: ORAI } }, { token: { contract_addr: tronCw20Address } }], + symbols: ["ORAI", "WTRX"] + }, + { + asset_infos: [{ token: { contract_addr: scAtomCw20Address } }, { native_token: { denom: atomIbcDenom } }], + symbols: ["scATOM", "ATOM"] + } + ]); }); - it.each<[[AssetInfo, AssetInfo], AssetInfo, number]>([ - [ - [{ native_token: { denom: ORAI } }, { native_token: { denom: atomIbcDenom } }], - { native_token: { denom: atomIbcDenom } }, - 0 - ], - [ - [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdtCw20Address } }], - { native_token: { denom: ORAI } }, - 1 - ], - [ - [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdcCw20Address } }], - { native_token: { denom: ORAI } }, - 1 - ], - [ - [{ token: { contract_addr: tronCw20Address } }, { native_token: { denom: atomIbcDenom } }], - { token: { contract_addr: tronCw20Address } }, - 1 - ] - ])("test-findUsdOraiInPair", (infos, expectedInfo, expectedBase) => { - // act - const result = findUsdOraiInPair(infos); - // assert - expect(result.target).toEqual(expectedInfo); - expect(result.baseIndex).toEqual(expectedBase); - }); + // it("test-calculatePriceByPool", () => { + // const result = calculatePriceByPool(BigInt(10305560305234), BigInt(10205020305234), 0); + // expect(result).toEqual(BigInt(9902432)); + // }); + + // it.each<[[AssetInfo, AssetInfo], AssetInfo, number]>([ + // [ + // [{ native_token: { denom: ORAI } }, { native_token: { denom: atomIbcDenom } }], + // { native_token: { denom: atomIbcDenom } }, + // 0 + // ], + // [ + // [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdtCw20Address } }], + // { native_token: { denom: ORAI } }, + // 1 + // ], + // [ + // [{ native_token: { denom: ORAI } }, { token: { contract_addr: usdcCw20Address } }], + // { native_token: { denom: ORAI } }, + // 1 + // ], + // [ + // [{ token: { contract_addr: tronCw20Address } }, { native_token: { denom: atomIbcDenom } }], + // { token: { contract_addr: tronCw20Address } }, + // 1 + // ] + // ])("test-findUsdOraiInPair", (infos, expectedInfo, expectedBase) => { + // // act + // const result = findUsdOraiInPair(infos); + // // assert + // expect(result.target).toEqual(expectedInfo); + // expect(result.baseIndex).toEqual(expectedBase); + // }); }); From 6dd8d26b2b4878f226d67fce571e27fef79f7779 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 10:45:20 +0700 Subject: [PATCH 58/75] added auto build oraidex sync package start server --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d386fdb8..fa22d720 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "docs": "typedoc --entryPointStrategy expand --name 'Oraidex SDK' --readme none --tsconfig packages/contracts-sdk/tsconfig.json packages/contracts-sdk/src", "build": "tsc -p", "deploy": "yarn publish --access public", - "start:server": "npx ts-node packages/oraidex-server/src/index.ts" + "start:server": "yarn build packages/oraidex-sync/ && npx ts-node packages/oraidex-server/src/index.ts" }, "workspaces": [ From 441fde10658c2f38f9077d486d98834890c5c770 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 10:55:38 +0700 Subject: [PATCH 59/75] added fallback case query latest timestamp swap ops --- packages/oraidex-sync/src/db.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index adeb26bc..e3439fe0 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -203,6 +203,7 @@ export class DuckDb { async queryLatestTimestampSwapOps(): Promise { const latestTimestamp = await this.conn.all("SELECT timestamp from swap_ops_data order by timestamp desc limit 1"); + if (latestTimestamp.length === 0 || !latestTimestamp[0].timestamp) return new Date().toISOString(); // fallback case return latestTimestamp[0].timestamp as string; } From 2c64322049ab142974dd2d07e6575e8fef218e44 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 11:20:26 +0700 Subject: [PATCH 60/75] added apache arrow --- packages/oraidex-sync/package.json | 3 +- packages/oraidex-sync/src/db.ts | 9 +- yarn.lock | 145 ++++++++++++++++++++++++++++- 3 files changed, 154 insertions(+), 3 deletions(-) diff --git a/packages/oraidex-sync/package.json b/packages/oraidex-sync/package.json index c04a8414..66f97ff2 100644 --- a/packages/oraidex-sync/package.json +++ b/packages/oraidex-sync/package.json @@ -14,7 +14,8 @@ "@oraichain/common-contracts-sdk": "1.0.13", "@oraichain/cosmos-rpc-sync": "^1.0.5", "@oraichain/oraidex-contracts-sdk": "^1.0.13", - "duckdb-async": "^0.8.1" + "duckdb-async": "^0.8.1", + "apache-arrow": "^12.0.1" }, "devDependencies": { "@types/lodash": "^4.14.195" diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index e3439fe0..3ce48901 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -96,7 +96,14 @@ export class DuckDb { // store all the current pair infos of oraiDEX. Will be updated to the latest pair list after the sync is restarted async createPairInfosTable() { await this.conn.exec( - "CREATE TABLE IF NOT EXISTS pair_infos (firstAssetInfo VARCHAR, secondAssetInfo VARCHAR, commissionRate VARCHAR, pairAddr VARCHAR, liquidityAddr VARCHAR, oracleAddr VARCHAR,PRIMARY KEY (pairAddr) )" + `CREATE TABLE IF NOT EXISTS pair_infos ( + firstAssetInfo VARCHAR, + secondAssetInfo VARCHAR, + commissionRate VARCHAR, + pairAddr VARCHAR, + liquidityAddr VARCHAR, + oracleAddr VARCHAR, + PRIMARY KEY (pairAddr) )` ); } diff --git a/yarn.lock b/yarn.lock index 7059ff56..a0e1b587 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2138,6 +2138,16 @@ "@types/connect" "*" "@types/node" "*" +"@types/command-line-args@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.0.tgz#adbb77980a1cc376bb208e3f4142e907410430f6" + integrity sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA== + +"@types/command-line-usage@5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/command-line-usage/-/command-line-usage-5.0.2.tgz#ba5e3f6ae5a2009d466679cc431b50635bf1a064" + integrity sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg== + "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -2246,6 +2256,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.1.tgz#a6033a8718653c50ac4962977e14d0f984d9527d" integrity sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg== +"@types/node@18.14.5": + version "18.14.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.5.tgz#4a13a6445862159303fc38586598a9396fc408b3" + integrity sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw== + "@types/node@>=13.7.0": version "20.1.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.1.tgz#afc492e8dbe7f672dd3a13674823522b467a45ad" @@ -2261,6 +2276,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@types/pad-left@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/pad-left/-/pad-left-2.1.1.tgz#17d906fc75804e1cc722da73623f1d978f16a137" + integrity sha512-Xd22WCRBydkGSApl5Bw0PhAOHKSVjNL3E3AwzKaps96IMraPqy5BvZIsBVK6JLwdybUzjHnuWVwpDd0JjTfHXA== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -2454,6 +2474,22 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +apache-arrow@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/apache-arrow/-/apache-arrow-12.0.1.tgz#dffd865850d1d94896f1e1aa8332d586fb9e7de1" + integrity sha512-g17ARsc/KEAzViy8PEFsDBlL4ZLx3BesgQCplDLgUWtY0aFWNdEmfaZsbbXVRDfQ21D7vbUKtu0ZWNgcbxDrig== + dependencies: + "@types/command-line-args" "5.2.0" + "@types/command-line-usage" "5.0.2" + "@types/node" "18.14.5" + "@types/pad-left" "2.1.1" + command-line-args "5.2.1" + command-line-usage "6.1.3" + flatbuffers "23.3.3" + json-bignum "^0.0.3" + pad-left "^2.1.0" + tslib "^2.5.0" + "aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" @@ -2492,6 +2528,16 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^4.0.1, array-back@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== + array-differ@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" @@ -2864,7 +2910,7 @@ caniuse-lite@^1.0.30001503: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz#418aefeed9d024cd3129bfae0ccc782d4cb8f12b" integrity sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA== -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -3031,6 +3077,26 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +command-line-args@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-usage@6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957" + integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw== + dependencies: + array-back "^4.0.2" + chalk "^2.4.2" + table-layout "^1.0.2" + typical "^5.2.0" + common-ancestor-path@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" @@ -3299,6 +3365,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== +deep-extend@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deepmerge@^4.2.2: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" @@ -3740,6 +3811,13 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + find-up@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -3767,6 +3845,11 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== +flatbuffers@23.3.3: + version "23.3.3" + resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-23.3.3.tgz#23654ba7a98d4b866a977ae668fe4f8969f34a66" + integrity sha512-jmreOaAT1t55keaf+Z259Tvh8tR/Srry9K8dgCgvizhKSEr6gLGgaOJI2WFL5fkOpGOGRZwxUrlFn0GCmXUy6g== + follow-redirects@^1.14.0, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" @@ -4990,6 +5073,11 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-bignum@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/json-bignum/-/json-bignum-0.0.3.tgz#41163b50436c773d82424dbc20ed70db7604b8d7" + integrity sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg== + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -5190,6 +5278,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" @@ -6100,6 +6193,13 @@ pacote@^13.0.3, pacote@^13.6.1: ssri "^9.0.0" tar "^6.1.11" +pad-left@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pad-left/-/pad-left-2.1.0.tgz#16e6a3b2d44a8e138cb0838cc7cb403a4fc9e994" + integrity sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA== + dependencies: + repeat-string "^1.5.4" + pako@^2.0.2: version "2.1.0" resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" @@ -6550,6 +6650,16 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +reduce-flatten@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" + integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== + +repeat-string@^1.5.4: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -7047,6 +7157,16 @@ synchronized-promise@^0.3.1: dependencies: deasync "^0.1.15" +table-layout@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" + integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== + dependencies: + array-back "^4.0.1" + deep-extend "~0.6.0" + typical "^5.2.0" + wordwrapjs "^4.0.0" + tar-stream@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" @@ -7217,6 +7337,11 @@ tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== +tslib@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3" + integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA== + type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -7282,6 +7407,16 @@ typedoc@^0.24.7: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" + integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== + uglify-js@^3.1.4: version "3.17.4" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" @@ -7488,6 +7623,14 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== +wordwrapjs@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" + integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== + dependencies: + reduce-flatten "^2.0.0" + typical "^5.2.0" + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" From e433e3ed69ab4472a59fedb0733c49a9ba3f0df1 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 11:41:56 +0700 Subject: [PATCH 61/75] changed to parameter query queryAllVolumeRange --- packages/oraidex-sync/src/db.ts | 16 +++++++----- packages/oraidex-sync/tests/db.spec.ts | 36 ++++++++++++-------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 3ce48901..8aaf13f6 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -184,20 +184,24 @@ export class DuckDb { from swap_ops_data where offerDenom = ? and askDenom = ? - and timestamp >= '${startTime}'::TIMESTAMP - and timestamp <= '${endTime}'::TIMESTAMP`, + and timestamp >= ?::TIMESTAMP + and timestamp <= ?::TIMESTAMP`, offerDenom, - askDenom + askDenom, + startTime, + endTime ), this.conn.all( `SELECT sum(offerAmount) as ${modifiedAskDenom}, sum(returnAmount) as ${modifiedOfferDenom} from swap_ops_data where offerDenom = ? and askDenom = ? - and timestamp >= '${startTime}'::TIMESTAMP - and timestamp <= '${endTime}'::TIMESTAMP`, + and timestamp >= ?::TIMESTAMP + and timestamp <= ?::TIMESTAMP`, askDenom, - offerDenom + offerDenom, + startTime, + endTime ) ]) ).flat(); diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index 4695150d..b639092a 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -1,16 +1,20 @@ import { DuckDb } from "../src/db"; -import { toDisplay } from "../src/helper"; describe("test-duckdb", () => { let duckDb: DuckDb; - it.each<[string, number]>([ - ["hello", 10001] - // ["orai", 100], - // ["foo", 10] + it.each<[string[], number[]]>([ + [ + ["orai", "atom"], + [121, 10012] + ], + [ + ["atom", "orai"], + [10012, 121] + ] ])( "test-duckdb-queryAllVolume-should-return-correct-total-volume-given-%s-should-have-%d", - async (denom, expectedVolume) => { + async (denoms, expectedVolumes) => { duckDb = await DuckDb.create(":memory:"); await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); await duckDb.insertSwapOps([ @@ -59,15 +63,9 @@ describe("test-duckdb", () => { txhash: "foo" } ]); - let queryResult = await duckDb.queryAllVolume("orai", "atom"); - console.log("query result: ", queryResult); - expect(queryResult.volume["orai"]).toEqual(121); - expect(queryResult.volume["atom"]).toEqual(10012); - - queryResult = await duckDb.queryAllVolume("atom", "orai"); - console.log("query result: ", queryResult); - expect(queryResult.volume["orai"]).toEqual(121); - expect(queryResult.volume["atom"]).toEqual(10012); + let queryResult = await duckDb.queryAllVolume(denoms[0], denoms[1]); + expect(queryResult.volume[denoms[0]]).toEqual(expectedVolumes[0]); + expect(queryResult.volume[denoms[1]]).toEqual(expectedVolumes[1]); } ); @@ -100,9 +98,9 @@ describe("test-duckdb", () => { { askDenom: "orai", commissionAmount: 0, - offerAmount: 10, + offerAmount: 100000, offerDenom: "atom", - returnAmount: 1, + returnAmount: 10000, spreadAmount: 0, taxAmount: 0, timestamp: new Date(1389610068000).toISOString(), @@ -111,9 +109,9 @@ describe("test-duckdb", () => { { askDenom: "atom", commissionAmount: 0, - offerAmount: 10, + offerAmount: 1000000, offerDenom: "orai", - returnAmount: 1, + returnAmount: 10000, spreadAmount: 0, taxAmount: 0, timestamp: new Date(1389610068000).toISOString(), From fc67c4cc2d3fca0a66378889b7876fe51bc601e5 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 12:03:25 +0700 Subject: [PATCH 62/75] added tx heights in tables --- packages/oraidex-sync/src/db.ts | 27 ++++++++++++++++--- packages/oraidex-sync/src/tx-parsing.ts | 36 +++++++++++++------------ packages/oraidex-sync/src/types.ts | 1 + packages/oraidex-sync/tests/db.spec.ts | 33 +++++++++++++++-------- 4 files changed, 66 insertions(+), 31 deletions(-) diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 8aaf13f6..71011755 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -58,7 +58,8 @@ export class DuckDb { spreadAmount UBIGINT, taxAmount UBIGINT, timestamp TIMESTAMP, - txhash VARCHAR)` + txhash VARCHAR, + txheight UINTEGER)` ); } @@ -85,7 +86,8 @@ export class DuckDb { secondTokenLp UBIGINT, timestamp TIMESTAMP, txCreator VARCHAR, - txhash VARCHAR)` + txhash VARCHAR, + txheight UINTEGER)` ); } @@ -113,7 +115,11 @@ export class DuckDb { async createPriceInfoTable() { await this.conn.exec( - "CREATE TABLE IF NOT EXISTS price_infos (txheight UINTEGER, timestamp TIMESTAMP, assetInfo VARCHAR, price UINTEGER)" + `CREATE TABLE IF NOT EXISTS price_infos ( + txheight UINTEGER, + timestamp TIMESTAMP, + assetInfo VARCHAR, + price UINTEGER)` ); } @@ -231,4 +237,19 @@ export class DuckDb { (data) => data as PairInfoData ); } + + // handle total liquidity history of oraidex + // store all the total liquidity history of oraiDEX. Total history liquidity is inserted when syncing + async createTotalLiquidityHistoryTable() { + await this.conn.exec( + `CREATE TABLE IF NOT EXISTS total_lp_history ( + firstAssetInfo VARCHAR, + secondAssetInfo VARCHAR, + commissionRate VARCHAR, + pairAddr VARCHAR, + liquidityAddr VARCHAR, + oracleAddr VARCHAR, + PRIMARY KEY (pairAddr) )` + ); + } } diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 93a4b478..967556b7 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -5,6 +5,7 @@ import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; import { Tx as CosmosTx } from "cosmjs-types/cosmos/tx/v1beta1/tx"; import { AccountTx, + BasicTxData, ModifiedMsgExecuteContract, MsgExecuteContractWithLogs, MsgType, @@ -44,7 +45,7 @@ function parseTxToMsgExecuteContractMsgs(tx: Tx): MsgExecuteContractWithLogs[] { return msgs; } -function extractSwapOperations(txhash: string, timestamp: string, events: readonly Event[]): SwapOperationData[] { +function extractSwapOperations(txData: BasicTxData, events: readonly Event[]): SwapOperationData[] { const wasmAttributes = parseWasmEvents(events); let swapData: SwapOperationData[] = []; let offerDenoms: string[] = []; @@ -92,16 +93,16 @@ function extractSwapOperations(txhash: string, timestamp: string, events: readon returnAmount: parseInt(returnAmounts[i]), spreadAmount: parseInt(spreadAmounts[i]), taxAmount: parseInt(taxAmounts[i]), - timestamp, - txhash + timestamp: txData.timestamp, + txhash: txData.txhash, + txheight: txData.txheight }); } return swapData; } function extractMsgProvideLiquidity( - txhash: string, - timestamp: string, + txData: BasicTxData, msg: MsgType, txCreator: string ): ProvideLiquidityOperationData | undefined { @@ -116,9 +117,10 @@ function extractMsgProvideLiquidity( secondTokenAmount: parseInt(secAsset.amount), secondTokenDenom: parseAssetInfoOnlyDenom(secAsset.info), secondTokenLp: parseInt(secAsset.amount), - timestamp, + timestamp: txData.timestamp, txCreator, - txhash + txhash: txData.txhash, + txheight: txData.txheight }; } return undefined; @@ -133,8 +135,7 @@ function parseWithdrawLiquidityAssets(assets: string): string[] { } function extractMsgWithdrawLiquidity( - txhash: string, - timestamp: string, + txData: BasicTxData, events: readonly Event[], txCreator: string ): WithdrawLiquidityOperationData[] { @@ -156,9 +157,10 @@ function extractMsgWithdrawLiquidity( secondTokenAmount: parseInt(assets[2]), secondTokenDenom: assets[3], secondTokenLp: parseInt(assets[2]), - timestamp, - txhash, - txCreator + timestamp: txData.timestamp, + txhash: txData.txhash, + txCreator, + txheight: txData.txheight }); } return withdrawData; @@ -205,14 +207,14 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { transactions.push(tx); const msgExecuteContracts = parseTxToMsgExecuteContractMsgs(tx); const msgs = parseExecuteContractToOraidexMsgs(msgExecuteContracts); - const txhash = tx.hash; + const basicTxData: BasicTxData = { timestamp: tx.timestamp, txhash: tx.hash, txheight: tx.height }; for (let msg of msgs) { const sender = msg.sender; - swapOpsData.push(...extractSwapOperations(txhash, tx.timestamp, msg.logs.events)); - const provideLiquidityData = extractMsgProvideLiquidity(txhash, tx.timestamp, msg.msg, sender); + swapOpsData.push(...extractSwapOperations(basicTxData, msg.logs.events)); + const provideLiquidityData = extractMsgProvideLiquidity(basicTxData, msg.msg, sender); if (provideLiquidityData) provideLiquidityOpsData.push(provideLiquidityData); - withdrawLiquidityOpsData.push(...extractMsgWithdrawLiquidity(txhash, tx.timestamp, msg.logs.events, sender)); - accountTxs.push({ txhash, accountAddress: sender }); + withdrawLiquidityOpsData.push(...extractMsgWithdrawLiquidity(basicTxData, msg.logs.events, sender)); + accountTxs.push({ txhash: basicTxData.txhash, accountAddress: sender }); } } return { diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 9fabf9ab..b7a80c43 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -15,6 +15,7 @@ export type AssetData = { export type BasicTxData = { timestamp: string; txhash: string; + txheight: number; }; export type SwapOperationData = { diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index b639092a..1160af08 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -27,7 +27,8 @@ describe("test-duckdb", () => { spreadAmount: 0, taxAmount: 0, timestamp: new Date(1689610068000).toISOString(), - txhash: "foo" + txhash: "foo", + txheight: 1 }, { askDenom: "orai", @@ -38,7 +39,8 @@ describe("test-duckdb", () => { spreadAmount: 0, taxAmount: 0, timestamp: new Date(1589610068000).toISOString(), - txhash: "foo" + txhash: "foo", + txheight: 1 }, { askDenom: "atom", @@ -49,7 +51,8 @@ describe("test-duckdb", () => { spreadAmount: 0, taxAmount: 0, timestamp: new Date(1589610068000).toISOString(), - txhash: "foo" + txhash: "foo", + txheight: 1 }, { askDenom: "atom", @@ -60,7 +63,8 @@ describe("test-duckdb", () => { spreadAmount: 0, taxAmount: 0, timestamp: new Date(1589610068000).toISOString(), - txhash: "foo" + txhash: "foo", + txheight: 1 } ]); let queryResult = await duckDb.queryAllVolume(denoms[0], denoms[1]); @@ -82,7 +86,8 @@ describe("test-duckdb", () => { spreadAmount: 0, taxAmount: 0, timestamp: new Date("2023-07-17T16:07:48.000Z").toISOString(), - txhash: "foo" + txhash: "foo", + txheight: 1 }, { askDenom: "atom", @@ -93,7 +98,8 @@ describe("test-duckdb", () => { spreadAmount: 0, taxAmount: 0, timestamp: new Date("2023-07-16T16:07:48.000Z").toISOString(), - txhash: "foo" + txhash: "foo", + txheight: 1 }, { askDenom: "orai", @@ -104,7 +110,8 @@ describe("test-duckdb", () => { spreadAmount: 0, taxAmount: 0, timestamp: new Date(1389610068000).toISOString(), - txhash: "foo" + txhash: "foo", + txheight: 1 }, { askDenom: "atom", @@ -115,7 +122,8 @@ describe("test-duckdb", () => { spreadAmount: 0, taxAmount: 0, timestamp: new Date(1389610068000).toISOString(), - txhash: "foo" + txhash: "foo", + txheight: 1 } ]); let queryResult = await duckDb.queryAllVolumeRange( @@ -155,7 +163,8 @@ describe("test-duckdb", () => { secondTokenLp: 0, secondTokenDenom: "atom", txCreator: "foobar", - opType: "provide" + opType: "provide", + txheight: 1 } ]) ).rejects.toThrow(); @@ -178,7 +187,8 @@ describe("test-duckdb", () => { secondTokenLp: 0, timestamp: newDate, txCreator: "foobar", - txhash: "foo" + txhash: "foo", + txheight: 1 } ]); let queryResult = await duckDb.queryLpOps(); @@ -193,7 +203,8 @@ describe("test-duckdb", () => { secondTokenLp: 0, secondTokenDenom: "atom", txCreator: "foobar", - opType: "withdraw" + opType: "withdraw", + txheight: 1 }); }); From b908cb8e9880ea59fd9bc0a6ced724f1b67e2866 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 15:44:04 +0700 Subject: [PATCH 63/75] fixed withdraw lp insert fix --- packages/oraidex-sync/package.json | 2 +- packages/oraidex-sync/src/db.ts | 20 +++++++---------- packages/oraidex-sync/src/index.ts | 24 ++++++-------------- packages/oraidex-sync/src/only-sync.ts | 11 ++++++++++ packages/oraidex-sync/src/tx-parsing.ts | 15 ++++++------- packages/oraidex-sync/tests/db.spec.ts | 29 +++++++++++++------------ yarn.lock | 7 ++++++ 7 files changed, 56 insertions(+), 52 deletions(-) create mode 100644 packages/oraidex-sync/src/only-sync.ts diff --git a/packages/oraidex-sync/package.json b/packages/oraidex-sync/package.json index 66f97ff2..bb34cef0 100644 --- a/packages/oraidex-sync/package.json +++ b/packages/oraidex-sync/package.json @@ -12,7 +12,7 @@ "dependencies": { "@cosmjs/tendermint-rpc": "^0.31.0", "@oraichain/common-contracts-sdk": "1.0.13", - "@oraichain/cosmos-rpc-sync": "^1.0.5", + "@oraichain/cosmos-rpc-sync": "^1.0.6", "@oraichain/oraidex-contracts-sdk": "^1.0.13", "duckdb-async": "^0.8.1", "apache-arrow": "^12.0.1" diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 71011755..11314f7d 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -238,18 +238,14 @@ export class DuckDb { ); } - // handle total liquidity history of oraidex - // store all the total liquidity history of oraiDEX. Total history liquidity is inserted when syncing - async createTotalLiquidityHistoryTable() { - await this.conn.exec( - `CREATE TABLE IF NOT EXISTS total_lp_history ( - firstAssetInfo VARCHAR, - secondAssetInfo VARCHAR, - commissionRate VARCHAR, - pairAddr VARCHAR, - liquidityAddr VARCHAR, - oracleAddr VARCHAR, - PRIMARY KEY (pairAddr) )` + async queryAllLp() { + // const result = await this.conn.all( + // "SELECT time_bucket(INTERVAL '5 minutes', timestamp) as dateTime, sum(firstTokenAmount + secondTokenAmount) as liquidity from lp_ops_data group by dateTime order by dateTime limit 3" + // ); + // console.log("time bucket: ", result); + const pivotResult = await this.conn.all( + "with pivot_lp_ops as ( pivot lp_ops_data on opType using sum(firstTokenAmount + secondTokenAmount) as liquidity ) SELECT time_bucket(INTERVAL '5 minutes', timestamp) as dateTime, sum(provide_liquidity) as liquidity from pivot_lp_ops group by dateTime order by dateTime" ); + console.log("pivot result: ", pivotResult); } } diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 67802de8..48c88f94 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -40,11 +40,9 @@ class WriteOrders extends WriteData { private async insertParsedTxs(txs: TxAnlysisResult) { // insert swap ops - await Promise.all([ - this.insertSwapOps(txs.swapOpsData), - this.insertLiquidityOps(txs.provideLiquidityOpsData), - this.insertLiquidityOps(txs.withdrawLiquidityOpsData) - ]); + await Promise.all([this.insertSwapOps(txs.swapOpsData), this.insertLiquidityOps(txs.provideLiquidityOpsData)]); + // has to split this out because they are sharing the same table, will clash when inserting + await this.insertLiquidityOps(txs.withdrawLiquidityOpsData); } private async queryLpOps(): Promise { @@ -165,11 +163,11 @@ class OraiDexSync { ]); let currentInd = await this.duckDb.loadHeightSnapshot(); let initialData: InitialData = { tokenPrices: [], blockHeader: undefined }; - // console.log("current ind: ", currentInd); // // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic - // if (currentInd <= 12388825) { - // currentInd = 12388825; - // } + if (currentInd <= 12508025) { + currentInd = 12508025; + } + console.log("current ind: ", currentInd); // const tokenPrices = await Promise.all( // extractUniqueAndFlatten(pairs).map((info) => this.simulateSwapPrice(info, currentInd)) @@ -192,14 +190,6 @@ class OraiDexSync { } } -// const start = async () => { -// const duckDb = await DuckDb.create("oraidex-sync-data"); -// const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); -// await oraidexSync.sync(); -// }; - -// start(); - export { OraiDexSync }; export * from "./types"; diff --git a/packages/oraidex-sync/src/only-sync.ts b/packages/oraidex-sync/src/only-sync.ts new file mode 100644 index 00000000..ae68b05b --- /dev/null +++ b/packages/oraidex-sync/src/only-sync.ts @@ -0,0 +1,11 @@ +import { OraiDexSync } from "src"; +import { DuckDb } from "./db"; +import "dotenv/config"; + +const start = async () => { + const duckDb = await DuckDb.create(process.env.DUCKDB_FILENAME || ":memory:"); + const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); + await oraidexSync.sync(); +}; + +start(); diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 967556b7..94a412b1 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -45,8 +45,7 @@ function parseTxToMsgExecuteContractMsgs(tx: Tx): MsgExecuteContractWithLogs[] { return msgs; } -function extractSwapOperations(txData: BasicTxData, events: readonly Event[]): SwapOperationData[] { - const wasmAttributes = parseWasmEvents(events); +function extractSwapOperations(txData: BasicTxData, wasmAttributes: (readonly Attribute[])[]): SwapOperationData[] { let swapData: SwapOperationData[] = []; let offerDenoms: string[] = []; let askDenoms: string[] = []; @@ -136,14 +135,13 @@ function parseWithdrawLiquidityAssets(assets: string): string[] { function extractMsgWithdrawLiquidity( txData: BasicTxData, - events: readonly Event[], + wasmAttributes: (readonly Attribute[])[], txCreator: string ): WithdrawLiquidityOperationData[] { const withdrawData: WithdrawLiquidityOperationData[] = []; - const wasmAttributes = parseWasmEvents(events); for (let attrs of wasmAttributes) { - if (!attrs.find((attr) => attr.key === "withdraw_liquidity")) continue; + if (!attrs.find((attr) => attr.key === "action" && attr.value === "withdraw_liquidity")) continue; const assetAttr = attrs.find((attr) => attr.key === "refund_assets"); if (!assetAttr) continue; const assets = parseWithdrawLiquidityAssets(assetAttr.value); @@ -158,8 +156,8 @@ function extractMsgWithdrawLiquidity( secondTokenDenom: assets[3], secondTokenLp: parseInt(assets[2]), timestamp: txData.timestamp, - txhash: txData.txhash, txCreator, + txhash: txData.txhash, txheight: txData.txheight }); } @@ -210,10 +208,11 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { const basicTxData: BasicTxData = { timestamp: tx.timestamp, txhash: tx.hash, txheight: tx.height }; for (let msg of msgs) { const sender = msg.sender; - swapOpsData.push(...extractSwapOperations(basicTxData, msg.logs.events)); + const wasmAttributes = parseWasmEvents(msg.logs.events); + swapOpsData.push(...extractSwapOperations(basicTxData, wasmAttributes)); const provideLiquidityData = extractMsgProvideLiquidity(basicTxData, msg.msg, sender); if (provideLiquidityData) provideLiquidityOpsData.push(provideLiquidityData); - withdrawLiquidityOpsData.push(...extractMsgWithdrawLiquidity(basicTxData, msg.logs.events, sender)); + withdrawLiquidityOpsData.push(...extractMsgWithdrawLiquidity(basicTxData, wasmAttributes, sender)); accountTxs.push({ txhash: basicTxData.txhash, accountAddress: sender }); } } diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index 1160af08..a126f094 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -208,18 +208,19 @@ describe("test-duckdb", () => { }); }); - // it("test-realdb", async () => { - // duckDb = await DuckDb.create("oraidex-sync-data"); - // const latestTimestamp = await duckDb.queryLatestTimestampSwapOps(); - // const now = new Date(latestTimestamp); - // function getDate24hBeforeNow(time: Date) { - // const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000; // 24 hours in milliseconds - // const date24hBeforeNow = new Date(time.getTime() - twentyFourHoursInMilliseconds); - // return date24hBeforeNow; - // } - // const then = getDate24hBeforeNow(now).toISOString(); - // // const baseVolume = await duckDb.queryAllVolumeRange("orai", then, now.toISOString()); - // const baseVolume = await duckDb.queryAllVolume("orai"); - // console.log("base volume: ", toDisplay(BigInt(baseVolume.volume))); - // }); + it("test-realdb", async () => { + duckDb = await DuckDb.create("oraidex-sync-data"); + await duckDb.queryAllLp(); + // const latestTimestamp = await duckDb.queryLatestTimestampSwapOps(); + // const now = new Date(latestTimestamp); + // function getDate24hBeforeNow(time: Date) { + // const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + // const date24hBeforeNow = new Date(time.getTime() - twentyFourHoursInMilliseconds); + // return date24hBeforeNow; + // } + // const then = getDate24hBeforeNow(now).toISOString(); + // // const baseVolume = await duckDb.queryAllVolumeRange("orai", then, now.toISOString()); + // const baseVolume = await duckDb.queryAllVolume("orai"); + // console.log("base volume: ", toDisplay(BigInt(baseVolume.volume))); + }); }); diff --git a/yarn.lock b/yarn.lock index a0e1b587..a2dde450 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1952,6 +1952,13 @@ dependencies: "@cosmjs/stargate" "^0.31.0" +"@oraichain/cosmos-rpc-sync@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@oraichain/cosmos-rpc-sync/-/cosmos-rpc-sync-1.0.6.tgz#bdf59afce6d4308a3f7fec4723d40052b5ac9e7e" + integrity sha512-3LJgKT+lpTdThbpw6W1oZvgu3oAkep8SJ+qbE4tPYVaN4rD4SAx3IV0z8ccBPXgVs2t3yjaHfx9y++TQs7ZN0Q== + dependencies: + "@cosmjs/stargate" "^0.31.0" + "@parcel/watcher@2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" From 91ae20cd57f1f1eff4efb27d9540f98a68dec79c Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 15:45:51 +0700 Subject: [PATCH 64/75] fixed only sync import --- packages/oraidex-sync/src/only-sync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/oraidex-sync/src/only-sync.ts b/packages/oraidex-sync/src/only-sync.ts index ae68b05b..9a74813a 100644 --- a/packages/oraidex-sync/src/only-sync.ts +++ b/packages/oraidex-sync/src/only-sync.ts @@ -1,4 +1,4 @@ -import { OraiDexSync } from "src"; +import { OraiDexSync } from "./index"; import { DuckDb } from "./db"; import "dotenv/config"; From 2bea76d441adba0370a8ab4e41c3f753a39c6687 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 16:00:07 +0700 Subject: [PATCH 65/75] added duckdb filename env --- packages/oraidex-server/.env.example | 3 ++- packages/oraidex-server/src/index.ts | 2 +- packages/oraidex-sync/.env.example | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/oraidex-server/.env.example b/packages/oraidex-server/.env.example index 2f790ef3..f5a80469 100644 --- a/packages/oraidex-server/.env.example +++ b/packages/oraidex-server/.env.example @@ -1,2 +1,3 @@ PORT=2024 -RPC_URL=https://rpc.orai,io \ No newline at end of file +RPC_URL=https://rpc.orai,io +DUCKDB_PROD_FILENAME="oraidex-sync-data" \ No newline at end of file diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 88435080..e44af205 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -104,7 +104,7 @@ app.get("/tickers", async (req, res) => { app.listen(port, hostname, async () => { // sync data for the service to read // console.dir(pairInfos, { depth: null }); - duckDb = await DuckDb.create("oraidex-sync-data"); + duckDb = await DuckDb.create(process.env.DUCKDB_PROD_FILENAME || "oraidex-sync-data"); const oraidexSync = await OraiDexSync.create(duckDb, process.env.RPC_URL || "https://rpc.orai.io"); oraidexSync.sync(); console.log(`[server]: oraiDEX info server is running at http://${hostname}:${port}`); diff --git a/packages/oraidex-sync/.env.example b/packages/oraidex-sync/.env.example index 3bae3266..23dc918f 100644 --- a/packages/oraidex-sync/.env.example +++ b/packages/oraidex-sync/.env.example @@ -4,4 +4,5 @@ FACTORY_CONTACT_ADDRESS_V2="orai167r4ut7avvgpp3rlzksz6vw5spmykluzagvmj3ht845fjsc ROUTER_CONTRACT_ADDRESS="orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" MULTICALL_CONTRACT_ADDRESS="orai1q7x644gmf7h8u8y6y8t9z9nnwl8djkmspypr6mxavsk9ual7dj0sxpmgwd" LIMIT=1000 -MAX_THREAD_LEVEL=3 \ No newline at end of file +MAX_THREAD_LEVEL=3 +DUCKDB_FILENAME="oraidex-sync-data-staging" \ No newline at end of file From 69599f3b95b49b5b005b145f34f4fc675a96dd7f Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 16:02:17 +0700 Subject: [PATCH 66/75] added initial sync height env --- packages/oraidex-sync/.env.example | 3 ++- packages/oraidex-sync/src/index.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/oraidex-sync/.env.example b/packages/oraidex-sync/.env.example index 23dc918f..9508a4e4 100644 --- a/packages/oraidex-sync/.env.example +++ b/packages/oraidex-sync/.env.example @@ -5,4 +5,5 @@ ROUTER_CONTRACT_ADDRESS="orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y MULTICALL_CONTRACT_ADDRESS="orai1q7x644gmf7h8u8y6y8t9z9nnwl8djkmspypr6mxavsk9ual7dj0sxpmgwd" LIMIT=1000 MAX_THREAD_LEVEL=3 -DUCKDB_FILENAME="oraidex-sync-data-staging" \ No newline at end of file +DUCKDB_FILENAME="oraidex-sync-data-staging" +INITIAL_SYNC_HEIGHT=12388825 \ No newline at end of file diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 48c88f94..a6b72b7b 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -163,9 +163,10 @@ class OraiDexSync { ]); let currentInd = await this.duckDb.loadHeightSnapshot(); let initialData: InitialData = { tokenPrices: [], blockHeader: undefined }; + const initialSyncHeight = parseInt(process.env.INITIAL_SYNC_HEIGHT) || 12388825; // // if its' the first time, then we use the height 12388825 since its the safe height for the rpc nodes to include timestamp & new indexing logic - if (currentInd <= 12508025) { - currentInd = 12508025; + if (currentInd <= initialSyncHeight) { + currentInd = initialSyncHeight; } console.log("current ind: ", currentInd); From 6beb4f09a30f92566378fa7f9077b97f18b359db Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 16:21:58 +0700 Subject: [PATCH 67/75] added close before create to apply WAL into db --- packages/oraidex-sync/src/db.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 11314f7d..922c96af 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -11,12 +11,18 @@ import fs, { rename } from "fs"; import { renameKey, replaceAllNonAlphaBetChar } from "./helper"; export class DuckDb { - protected constructor(public readonly conn: Connection) {} + protected constructor(public readonly conn: Connection, private db: Database) {} static async create(fileName?: string): Promise { - const db = await Database.create(fileName ?? "data"); + let db = await Database.create(fileName ?? "data"); + await db.close(); // close to flush WAL file + db = await Database.create(fileName ?? "data"); const conn = await db.connect(); - return new DuckDb(conn); + return new DuckDb(conn, db); + } + + async closeDb() { + this.db.close(); } private async insertBulkData(data: any[], tableName: string, replace?: boolean, fileName?: string) { @@ -244,7 +250,7 @@ export class DuckDb { // ); // console.log("time bucket: ", result); const pivotResult = await this.conn.all( - "with pivot_lp_ops as ( pivot lp_ops_data on opType using sum(firstTokenAmount + secondTokenAmount) as liquidity ) SELECT time_bucket(INTERVAL '5 minutes', timestamp) as dateTime, sum(provide_liquidity) as liquidity from pivot_lp_ops group by dateTime order by dateTime" + "with pivot_lp_ops as ( pivot lp_ops_data on opType using sum(firstTokenAmount + secondTokenAmount) as liquidity ) SELECT time_bucket(INTERVAL '5 minutes', timestamp) as dateTime, sum(provide_liquidity + withdraw_liquidity) as liquidity from pivot_lp_ops group by dateTime order by dateTime" ); console.log("pivot result: ", pivotResult); } From 65162760746dcbb374fc6e19d71c669c24f6b308 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 19 Jul 2023 18:12:06 +0700 Subject: [PATCH 68/75] added query total lp within range given tf --- packages/oraidex-sync/src/db.ts | 36 ++++++++++++++++++++------ packages/oraidex-sync/src/test-db.ts | 12 +++++++++ packages/oraidex-sync/src/types.ts | 5 ++++ packages/oraidex-sync/tests/db.spec.ts | 16 ------------ 4 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 packages/oraidex-sync/src/test-db.ts diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 922c96af..362eeedc 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -4,6 +4,7 @@ import { PriceInfo, SwapOperationData, TokenVolumeData, + TotalLiquidity, VolumeData, WithdrawLiquidityOperationData } from "./types"; @@ -244,14 +245,33 @@ export class DuckDb { ); } - async queryAllLp() { - // const result = await this.conn.all( - // "SELECT time_bucket(INTERVAL '5 minutes', timestamp) as dateTime, sum(firstTokenAmount + secondTokenAmount) as liquidity from lp_ops_data group by dateTime order by dateTime limit 3" - // ); - // console.log("time bucket: ", result); - const pivotResult = await this.conn.all( - "with pivot_lp_ops as ( pivot lp_ops_data on opType using sum(firstTokenAmount + secondTokenAmount) as liquidity ) SELECT time_bucket(INTERVAL '5 minutes', timestamp) as dateTime, sum(provide_liquidity + withdraw_liquidity) as liquidity from pivot_lp_ops group by dateTime order by dateTime" + /** + * query total lp with time frame. negative lp is withdraw. COALESCE - if null then use default value + * @param tf timeframe in seconds + * @param startTime start time in iso format + * @param endTime end time in iso format + */ + async queryTotalLpTimeFrame(tf: number, startTime: string, endTime: string): Promise { + const result = await this.conn.all( + `with pivot_lp_ops as ( + pivot lp_ops_data + on opType + using sum(firstTokenAmount + secondTokenAmount) as liquidity ) + SELECT (epoch(timestamp) // ?) as time, + sum(COALESCE(provide_liquidity,0) - COALESCE(withdraw_liquidity, 0)) as liquidity + from pivot_lp_ops + where timestamp >= ?::TIMESTAMP + and timestamp <= ?::TIMESTAMP + group by time + order by time`, + tf, + startTime, + endTime ); - console.log("pivot result: ", pivotResult); + // reset time to iso format after dividing in the query + result.forEach((item) => { + item.time = new Date(parseInt((item.time * tf * 1000).toFixed(0))).toISOString(); + }); + return result as TotalLiquidity[]; } } diff --git a/packages/oraidex-sync/src/test-db.ts b/packages/oraidex-sync/src/test-db.ts new file mode 100644 index 00000000..5a741c3a --- /dev/null +++ b/packages/oraidex-sync/src/test-db.ts @@ -0,0 +1,12 @@ +import { DuckDb } from "./db"; + +const start = async () => { + const duckDb = await DuckDb.create("oraidex-sync-data-test"); + await duckDb.queryTotalLpTimeFrame( + 86400, + new Date("2023-07-06T00:00:00.000Z").toISOString(), + new Date("2023-07-12T00:00:00.000Z").toISOString() + ); +}; + +start(); diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index b7a80c43..4b352cb5 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -146,3 +146,8 @@ export type TickerInfo = { target: string; pool_id: string; }; + +export type TotalLiquidity = { + time: string; + liquidity: number; +}; diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index a126f094..a5c7deee 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -207,20 +207,4 @@ describe("test-duckdb", () => { txheight: 1 }); }); - - it("test-realdb", async () => { - duckDb = await DuckDb.create("oraidex-sync-data"); - await duckDb.queryAllLp(); - // const latestTimestamp = await duckDb.queryLatestTimestampSwapOps(); - // const now = new Date(latestTimestamp); - // function getDate24hBeforeNow(time: Date) { - // const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000; // 24 hours in milliseconds - // const date24hBeforeNow = new Date(time.getTime() - twentyFourHoursInMilliseconds); - // return date24hBeforeNow; - // } - // const then = getDate24hBeforeNow(now).toISOString(); - // // const baseVolume = await duckDb.queryAllVolumeRange("orai", then, now.toISOString()); - // const baseVolume = await duckDb.queryAllVolume("orai"); - // console.log("base volume: ", toDisplay(BigInt(baseVolume.volume))); - }); }); From 77c84c16a295427b3d4b2804a61e859ad8aa65d4 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 20 Jul 2023 11:52:20 +0700 Subject: [PATCH 69/75] added liquidity history query --- packages/oraidex-server/src/index.ts | 67 ++++++++++++++++++++++++++-- packages/oraidex-sync/src/db.ts | 7 +-- packages/oraidex-sync/src/index.ts | 2 +- packages/oraidex-sync/src/query.ts | 2 +- packages/oraidex-sync/src/test-db.ts | 16 ++++--- packages/oraidex-sync/src/types.ts | 1 + 6 files changed, 82 insertions(+), 13 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index e44af205..f37df4a5 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -8,12 +8,15 @@ import { findPairAddress, toDisplay, OraiDexSync, - simulateSwapPrice + simulateSwapPrice, + getPoolInfos, + calculatePrefixSum } from "@oraichain/oraidex-sync"; import cors from "cors"; import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; -import { OraiswapRouterQueryClient } from "@oraichain/oraidex-contracts-sdk"; +import { OraiswapRouterQueryClient, PairInfo } from "@oraichain/oraidex-contracts-sdk"; import { getDate24hBeforeNow, parseSymbolsToTickerId } from "./helper"; +import { MulticallQueryClient } from "@oraichain/common-contracts-sdk"; dotenv.config(); @@ -47,6 +50,7 @@ app.get("/pairs", async (req, res) => { app.get("/tickers", async (req, res) => { try { + const { endTime } = req.query; const cosmwasmClient = await CosmWasmClient.connect(rpcUrl); const routerContract = new OraiswapRouterQueryClient( cosmwasmClient, @@ -62,7 +66,7 @@ app.get("/tickers", async (req, res) => { // const { baseIndex, targetIndex, target } = findUsdOraiInPair(pair.asset_infos); const baseIndex = 0; const targetIndex = 1; - const latestTimestamp = await duckDb.queryLatestTimestampSwapOps(); + const latestTimestamp = parseInt(endTime as string) ?? (await duckDb.queryLatestTimestampSwapOps()); const now = new Date(latestTimestamp); const then = getDate24hBeforeNow(now).toISOString(); const baseInfo = parseAssetInfoOnlyDenom(pair.asset_infos[baseIndex]); @@ -101,6 +105,63 @@ app.get("/tickers", async (req, res) => { } }); +// app.get("/liquidity/v2/historical/chart", async (req, res) => { +// let { start, end, tf } = req.query; +// // start, end time is timestamp in ms. tf is the in sec +// let timeframe: number; +// console.log(start, end, tf); + +// try { +// start = new Date(parseInt(start as string)).toISOString(); +// end = new Date(parseInt(end as string)).toISOString(); +// timeframe = parseInt(tf as string); +// } catch (error) { +// console.log("input error /liquidity/v2/historical/chart: ", error); +// res.status(400).send(error); +// return; +// } + +// try { +// const result = await duckDb.queryTotalLpTimeFrame(timeframe, start as string, end as string); +// const cosmwasmClient = await CosmWasmClient.connect(rpcUrl); +// cosmwasmClient.setQueryClientWithHeight(result[0].height); +// const multicall = new MulticallQueryClient( +// cosmwasmClient, +// process.env.MULTICALL_CONTRACT_ADDRES || "orai1q7x644gmf7h8u8y6y8t9z9nnwl8djkmspypr6mxavsk9ual7dj0sxpmgwd" +// ); +// const pairInfos = await duckDb.queryPairInfos(); +// const poolInfos = await getPoolInfos( +// pairInfos.map( +// (info) => +// ({ +// asset_infos: [JSON.parse(info.firstAssetInfo), JSON.parse(info.secondAssetInfo)], +// commission_rate: info.commissionRate, +// contract_addr: info.pairAddr, +// liquidity_token: info.liquidityAddr, +// oracle_addr: info.oracleAddr +// } as PairInfo) +// ), +// multicall +// ); +// let totalInitialLp = 0; +// for (let info of poolInfos) { +// totalInitialLp += parseInt(info.total_share); +// } +// console.log("total init lp: ", totalInitialLp); +// const prefixSum = calculatePrefixSum( +// totalInitialLp, +// result.map((res) => ({ ...res, denom: "", amount: res.liquidity })) +// ); +// console.log("prefix sum: ", prefixSum); +// res.status(200).send("hello world"); +// } catch (error) { +// console.log("server error /liquidity/v2/historical/chart: ", error); +// res.status(500).send(JSON.stringify(error)); +// } finally { +// return; +// } +// }); + app.listen(port, hostname, async () => { // sync data for the service to read // console.dir(pairInfos, { depth: null }); diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 362eeedc..0a06c31e 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -182,8 +182,8 @@ export class DuckDb { } async queryAllVolumeRange( - offerDenom: string, - askDenom: string, + offerDenom: string, // eg: orai + askDenom: string, // usdt startTime: string, endTime: string ): Promise { @@ -258,7 +258,8 @@ export class DuckDb { on opType using sum(firstTokenAmount + secondTokenAmount) as liquidity ) SELECT (epoch(timestamp) // ?) as time, - sum(COALESCE(provide_liquidity,0) - COALESCE(withdraw_liquidity, 0)) as liquidity + sum(COALESCE(provide_liquidity,0) - COALESCE(withdraw_liquidity, 0)) as liquidity, + any_value(txheight) as height from pivot_lp_ops where timestamp >= ?::TIMESTAMP and timestamp <= ?::TIMESTAMP diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index a6b72b7b..205e8a40 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -176,7 +176,7 @@ class OraiDexSync { // const initialBlockHeader = (await this.cosmwasmClient.getBlock(currentInd)).header; // initialData.tokenPrices = tokenPrices; // initialData.blockHeader = initialBlockHeader; - // await this.updateLatestPairInfos(); + await this.updateLatestPairInfos(); new SyncData({ offset: currentInd, rpcUrl: this.rpcUrl, diff --git a/packages/oraidex-sync/src/query.ts b/packages/oraidex-sync/src/query.ts index 3be3c8ff..b5eaa1f9 100644 --- a/packages/oraidex-sync/src/query.ts +++ b/packages/oraidex-sync/src/query.ts @@ -24,7 +24,7 @@ async function getPoolInfos(pairs: PairInfo[], multicall: MulticallReadOnlyInter }) }); // reset query client to latest for other functions to call - return res.return_data.map((data) => (data.success ? fromBinary(data.data) : undefined)); + return res.return_data.map((data) => (data.success ? fromBinary(data.data) : undefined)).filter((data) => data); // remove undefined items } async function getAllPairInfos( diff --git a/packages/oraidex-sync/src/test-db.ts b/packages/oraidex-sync/src/test-db.ts index 5a741c3a..4c926a79 100644 --- a/packages/oraidex-sync/src/test-db.ts +++ b/packages/oraidex-sync/src/test-db.ts @@ -1,12 +1,18 @@ import { DuckDb } from "./db"; +import { calculatePrefixSum } from "./helper"; const start = async () => { const duckDb = await DuckDb.create("oraidex-sync-data-test"); - await duckDb.queryTotalLpTimeFrame( - 86400, - new Date("2023-07-06T00:00:00.000Z").toISOString(), - new Date("2023-07-12T00:00:00.000Z").toISOString() - ); + const startTime = new Date("2023-07-06T00:00:00.000Z"); + const endTime = new Date("2023-07-12T00:00:00.000Z"); + console.log(startTime.getTime(), endTime.getTime()); + const result = await duckDb.queryTotalLpTimeFrame(86400, startTime.toISOString(), endTime.toISOString()); + console.log("result: ", result); + // const newData = calculatePrefixSum( + // 100000000000, + // result.map((res) => ({ denom: "", amount: res.liquidity })) + // ); + // console.log("new data: ", newData); }; start(); diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 4b352cb5..2784762e 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -150,4 +150,5 @@ export type TickerInfo = { export type TotalLiquidity = { time: string; liquidity: number; + height: number; }; From ba43790d25b921ed1b92263498944ce4ac28dffd Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 20 Jul 2023 11:53:51 +0700 Subject: [PATCH 70/75] fixed symbol osmosis to osmo --- packages/oraidex-sync/src/pairs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/oraidex-sync/src/pairs.ts b/packages/oraidex-sync/src/pairs.ts index b3ddd51d..49ee967c 100644 --- a/packages/oraidex-sync/src/pairs.ts +++ b/packages/oraidex-sync/src/pairs.ts @@ -50,7 +50,7 @@ export const pairs: PairMapping[] = [ native_token: { denom: osmosisIbcDenom } } ], - symbols: ["ORAI", "OSMOSIS"] + symbols: ["ORAI", "OSMO"] }, { asset_infos: [{ token: { contract_addr: milkyCw20Address } }, { token: { contract_addr: usdtCw20Address } }], From 9280f8789fb087e3a7503d9bda71dd6248a3b362 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 20 Jul 2023 11:54:42 +0700 Subject: [PATCH 71/75] fix endtime --- packages/oraidex-server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index f37df4a5..103f4fa9 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -66,7 +66,7 @@ app.get("/tickers", async (req, res) => { // const { baseIndex, targetIndex, target } = findUsdOraiInPair(pair.asset_infos); const baseIndex = 0; const targetIndex = 1; - const latestTimestamp = parseInt(endTime as string) ?? (await duckDb.queryLatestTimestampSwapOps()); + const latestTimestamp = endTime ? parseInt(endTime as string) : await duckDb.queryLatestTimestampSwapOps(); const now = new Date(latestTimestamp); const then = getDate24hBeforeNow(now).toISOString(); const baseInfo = parseAssetInfoOnlyDenom(pair.asset_infos[baseIndex]); From 291c2171e92c8903c7811dc092cbaaa8a84bd615 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 20 Jul 2023 12:03:06 +0700 Subject: [PATCH 72/75] changed 24h to counting from 00:00 --- packages/oraidex-server/src/helper.ts | 6 ++---- packages/oraidex-server/src/index.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/oraidex-server/src/helper.ts b/packages/oraidex-server/src/helper.ts index 44784e58..11e4b8f3 100644 --- a/packages/oraidex-server/src/helper.ts +++ b/packages/oraidex-server/src/helper.ts @@ -2,8 +2,6 @@ export function parseSymbolsToTickerId(symbols: [string, string]) { return `${symbols[0]}_${symbols[1]}`; } -export function getDate24hBeforeNow(time: Date) { - const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000; // 24 hours in milliseconds - const date24hBeforeNow = new Date(time.getTime() - twentyFourHoursInMilliseconds); - return date24hBeforeNow; +export function getDate24hBeforeNow() { + return new Date(new Date().setUTCHours(0, 0, 0, 0)); } diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 103f4fa9..929c6421 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -68,7 +68,7 @@ app.get("/tickers", async (req, res) => { const targetIndex = 1; const latestTimestamp = endTime ? parseInt(endTime as string) : await duckDb.queryLatestTimestampSwapOps(); const now = new Date(latestTimestamp); - const then = getDate24hBeforeNow(now).toISOString(); + const then = getDate24hBeforeNow().toISOString(); const baseInfo = parseAssetInfoOnlyDenom(pair.asset_infos[baseIndex]); const targetInfo = parseAssetInfoOnlyDenom(pair.asset_infos[targetIndex]); const volume = await duckDb.queryAllVolumeRange(baseInfo, targetInfo, then, now.toISOString()); From c67c71e541ec7c7b7cbe8a3f51085c88027a83a4 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 20 Jul 2023 12:45:25 +0700 Subject: [PATCH 73/75] reverted 24h volume --- packages/oraidex-server/src/helper.ts | 6 ++++-- packages/oraidex-server/src/index.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/oraidex-server/src/helper.ts b/packages/oraidex-server/src/helper.ts index 11e4b8f3..44784e58 100644 --- a/packages/oraidex-server/src/helper.ts +++ b/packages/oraidex-server/src/helper.ts @@ -2,6 +2,8 @@ export function parseSymbolsToTickerId(symbols: [string, string]) { return `${symbols[0]}_${symbols[1]}`; } -export function getDate24hBeforeNow() { - return new Date(new Date().setUTCHours(0, 0, 0, 0)); +export function getDate24hBeforeNow(time: Date) { + const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + const date24hBeforeNow = new Date(time.getTime() - twentyFourHoursInMilliseconds); + return date24hBeforeNow; } diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 929c6421..103f4fa9 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -68,7 +68,7 @@ app.get("/tickers", async (req, res) => { const targetIndex = 1; const latestTimestamp = endTime ? parseInt(endTime as string) : await duckDb.queryLatestTimestampSwapOps(); const now = new Date(latestTimestamp); - const then = getDate24hBeforeNow().toISOString(); + const then = getDate24hBeforeNow(now).toISOString(); const baseInfo = parseAssetInfoOnlyDenom(pair.asset_infos[baseIndex]); const targetInfo = parseAssetInfoOnlyDenom(pair.asset_infos[targetIndex]); const volume = await duckDb.queryAllVolumeRange(baseInfo, targetInfo, then, now.toISOString()); From dac440132e1e588c9ec7822491a69957dfa8ed06 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 20 Jul 2023 17:04:32 +0700 Subject: [PATCH 74/75] changed timestamp to number, added vol info & demo flow to query tf volume --- packages/oraidex-server/src/index.ts | 2 +- packages/oraidex-sync/src/db.ts | 48 +++++--- packages/oraidex-sync/src/helper.ts | 4 + packages/oraidex-sync/src/index.ts | 9 +- packages/oraidex-sync/src/query.ts | 5 +- packages/oraidex-sync/src/test-db.ts | 125 +++++++++++++++++++-- packages/oraidex-sync/src/tx-parsing.ts | 27 ++++- packages/oraidex-sync/src/types.ts | 12 +- packages/oraidex-sync/tests/db.spec.ts | 31 ++--- packages/oraidex-sync/tests/helper.spec.ts | 2 +- 10 files changed, 217 insertions(+), 48 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index 103f4fa9..c03fc73c 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -77,7 +77,7 @@ app.get("/tickers", async (req, res) => { base_currency: symbols[baseIndex], target_currency: symbols[targetIndex], last_price: "", - base_volume: toDisplay(BigInt(volume.volume[baseInfo])).toString(), // TODO: remove random + base_volume: toDisplay(BigInt(volume.volume[baseInfo])).toString(), target_volume: toDisplay(BigInt(volume.volume[targetInfo])).toString(), pool_id: pairAddr ?? "", base: symbols[baseIndex], diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index 0a06c31e..bc6e3cdd 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -6,6 +6,7 @@ import { TokenVolumeData, TotalLiquidity, VolumeData, + VolumeInfo, WithdrawLiquidityOperationData } from "./types"; import fs, { rename } from "fs"; @@ -64,7 +65,7 @@ export class DuckDb { returnAmount UBIGINT, spreadAmount UBIGINT, taxAmount UBIGINT, - timestamp TIMESTAMP, + timestamp UINTEGER, txhash VARCHAR, txheight UINTEGER)` ); @@ -91,7 +92,7 @@ export class DuckDb { secondTokenAmount UBIGINT, secondTokenDenom VARCHAR, secondTokenLp UBIGINT, - timestamp TIMESTAMP, + timestamp UINTEGER, txCreator VARCHAR, txhash VARCHAR, txheight UINTEGER)` @@ -124,7 +125,7 @@ export class DuckDb { await this.conn.exec( `CREATE TABLE IF NOT EXISTS price_infos ( txheight UINTEGER, - timestamp TIMESTAMP, + timestamp UINTEGER, assetInfo VARCHAR, price UINTEGER)` ); @@ -184,8 +185,8 @@ export class DuckDb { async queryAllVolumeRange( offerDenom: string, // eg: orai askDenom: string, // usdt - startTime: string, - endTime: string + startTime: number, + endTime: number ): Promise { // need to replace because the denom can contain numbers and other figures. We replace for temporary only, will be reverted once finish reducing const modifiedOfferDenom = replaceAllNonAlphaBetChar(offerDenom); @@ -197,8 +198,8 @@ export class DuckDb { from swap_ops_data where offerDenom = ? and askDenom = ? - and timestamp >= ?::TIMESTAMP - and timestamp <= ?::TIMESTAMP`, + and timestamp >= ? + and timestamp <= ?`, offerDenom, askDenom, startTime, @@ -209,8 +210,8 @@ export class DuckDb { from swap_ops_data where offerDenom = ? and askDenom = ? - and timestamp >= ?::TIMESTAMP - and timestamp <= ?::TIMESTAMP`, + and timestamp >= ? + and timestamp <= ?`, askDenom, offerDenom, startTime, @@ -225,10 +226,10 @@ export class DuckDb { }; } - async queryLatestTimestampSwapOps(): Promise { + async queryLatestTimestampSwapOps(): Promise { const latestTimestamp = await this.conn.all("SELECT timestamp from swap_ops_data order by timestamp desc limit 1"); - if (latestTimestamp.length === 0 || !latestTimestamp[0].timestamp) return new Date().toISOString(); // fallback case - return latestTimestamp[0].timestamp as string; + if (latestTimestamp.length === 0 || !latestTimestamp[0].timestamp) return new Date().getTime() / 1000; // fallback case + return latestTimestamp[0].timestamp as number; } async querySwapOps() { @@ -257,12 +258,12 @@ export class DuckDb { pivot lp_ops_data on opType using sum(firstTokenAmount + secondTokenAmount) as liquidity ) - SELECT (epoch(timestamp) // ?) as time, + SELECT (timestamp // ?) as time, sum(COALESCE(provide_liquidity,0) - COALESCE(withdraw_liquidity, 0)) as liquidity, any_value(txheight) as height from pivot_lp_ops - where timestamp >= ?::TIMESTAMP - and timestamp <= ?::TIMESTAMP + where timestamp >= ? + and timestamp <= ? group by time order by time`, tf, @@ -271,8 +272,23 @@ export class DuckDb { ); // reset time to iso format after dividing in the query result.forEach((item) => { - item.time = new Date(parseInt((item.time * tf * 1000).toFixed(0))).toISOString(); + item.time = parseInt((item.time * tf).toFixed(0)); }); return result as TotalLiquidity[]; } + + async createVolumeInfo() { + await this.conn.exec( + `CREATE TABLE IF NOT EXISTS volume_info ( + denom VARCHAR, + timestamp UINTEGER, + txheight UINTEGER, + volume UBIGINT) + ` + ); + } + + async insertVolumeInfo(volumeInfos: VolumeInfo[]) { + await this.insertBulkData(volumeInfos, "volume_info"); + } } diff --git a/packages/oraidex-sync/src/helper.ts b/packages/oraidex-sync/src/helper.ts index c4c968d8..6f5f0b4c 100644 --- a/packages/oraidex-sync/src/helper.ts +++ b/packages/oraidex-sync/src/helper.ts @@ -45,6 +45,10 @@ export const toDisplay = (amount: string | bigint, sourceDecimals = 6, desDecima return Number(returnAmount) / (displayDecimals === truncDecimals ? atomic : 10 ** displayDecimals); }; +export function isoToTimestampNumber(time: string) { + return new Date(time).getTime() / 1000; +} + export function renameKey(object: Object, oldKey: string, newKey: string): any { if (oldKey === newKey) return object; // Check if the old key exists in the object diff --git a/packages/oraidex-sync/src/index.ts b/packages/oraidex-sync/src/index.ts index 205e8a40..0840ba64 100644 --- a/packages/oraidex-sync/src/index.ts +++ b/packages/oraidex-sync/src/index.ts @@ -40,7 +40,11 @@ class WriteOrders extends WriteData { private async insertParsedTxs(txs: TxAnlysisResult) { // insert swap ops - await Promise.all([this.insertSwapOps(txs.swapOpsData), this.insertLiquidityOps(txs.provideLiquidityOpsData)]); + await Promise.all([ + this.insertSwapOps(txs.swapOpsData), + this.insertLiquidityOps(txs.provideLiquidityOpsData), + this.duckDb.insertVolumeInfo(txs.volumeInfos) + ]); // has to split this out because they are sharing the same table, will clash when inserting await this.insertLiquidityOps(txs.withdrawLiquidityOpsData); } @@ -159,7 +163,8 @@ class OraiDexSync { this.duckDb.createLiquidityOpsTable(), this.duckDb.createSwapOpsTable(), this.duckDb.createPairInfosTable(), - this.duckDb.createPriceInfoTable() + this.duckDb.createPriceInfoTable(), + this.duckDb.createVolumeInfo() ]); let currentInd = await this.duckDb.loadHeightSnapshot(); let initialData: InitialData = { tokenPrices: [], blockHeader: undefined }; diff --git a/packages/oraidex-sync/src/query.ts b/packages/oraidex-sync/src/query.ts index b5eaa1f9..e3f5da58 100644 --- a/packages/oraidex-sync/src/query.ts +++ b/packages/oraidex-sync/src/query.ts @@ -8,8 +8,8 @@ import { Asset, AssetInfo } from "@oraichain/oraidex-contracts-sdk"; import { MulticallReadOnlyInterface } from "@oraichain/common-contracts-sdk"; import { fromBinary, toBinary } from "@cosmjs/cosmwasm-stargate"; import { pairs } from "./pairs"; -import { findAssetInfoPathToUsdt, generateSwapOperations, toDisplay } from "./helper"; -import { tenAmountInDecimalSix } from "./constants"; +import { findAssetInfoPathToUsdt, generateSwapOperations, parseAssetInfoOnlyDenom, toDisplay } from "./helper"; +import { tenAmountInDecimalSix, usdtCw20Address } from "./constants"; async function getPoolInfos(pairs: PairInfo[], multicall: MulticallReadOnlyInterface): Promise { // adjust the query height to get data from the past @@ -48,6 +48,7 @@ async function getAllPairInfos( async function simulateSwapPriceWithUsdt(info: AssetInfo, router: OraiswapRouterReadOnlyInterface): Promise { // adjust the query height to get data from the past + if (parseAssetInfoOnlyDenom(info) === usdtCw20Address) return { info, amount: "1" }; const infoPath = findAssetInfoPathToUsdt(info); const amount = await simulateSwapPrice(infoPath, router); return { info, amount }; diff --git a/packages/oraidex-sync/src/test-db.ts b/packages/oraidex-sync/src/test-db.ts index 4c926a79..01fb7c74 100644 --- a/packages/oraidex-sync/src/test-db.ts +++ b/packages/oraidex-sync/src/test-db.ts @@ -1,13 +1,124 @@ +import { CosmWasmClient, OraiswapRouterQueryClient, SwapOperation } from "@oraichain/oraidex-contracts-sdk"; import { DuckDb } from "./db"; -import { calculatePrefixSum } from "./helper"; +import { SwapOperationData } from "./types"; +import { pairs, uniqueInfos } from "./pairs"; +import { parseAssetInfoOnlyDenom } from "./helper"; +import { simulateSwapPriceWithUsdt } from "./query"; +import "dotenv/config"; const start = async () => { - const duckDb = await DuckDb.create("oraidex-sync-data-test"); - const startTime = new Date("2023-07-06T00:00:00.000Z"); - const endTime = new Date("2023-07-12T00:00:00.000Z"); - console.log(startTime.getTime(), endTime.getTime()); - const result = await duckDb.queryTotalLpTimeFrame(86400, startTime.toISOString(), endTime.toISOString()); - console.log("result: ", result); + const duckdb = await DuckDb.create("oraidex-sync-data"); + // const result = await duckdb.conn.all( + // `SELECT (timestamp // ?) as time, + // any_value(txheight) as height, + // sum(volume) as volume, + // denom, + // from volume_info + // where denom = ? + // group by time, denom + // order by time`, + // 60, + // "orai" + // ); + // result.forEach((item) => { + // item.time = parseInt((item.time * 60).toFixed(0)); + // }); + // console.log(result); + let volumeInfos = await duckdb.conn.all( + "pivot volume_info on denom using sum(volume) group by timestamp, txheight order by timestamp" + ); + for (let volInfo of volumeInfos) { + for (const key in volInfo) { + if (volInfo[key] === null) volInfo[key] = 0; + } + } + console.log("volume infos: ", volumeInfos); + + const cosmwasmClient = await CosmWasmClient.connect(process.env.RPC_URL); + let finalArray = []; + let prices; + let heightCount = 0; + for (let i = 0; i < volumeInfos.length; i++) { + const volInfo = volumeInfos[i]; + cosmwasmClient.setQueryClientWithHeight(volInfo.txheight); + const router = new OraiswapRouterQueryClient( + cosmwasmClient, + "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" + ); + if (heightCount % 1000 === 0) { + // prevent simulating too many times. TODO: calculate this using pool data from + prices = (await Promise.all(uniqueInfos.map((info) => simulateSwapPriceWithUsdt(info, router)))) + .map((price) => ({ ...price, info: parseAssetInfoOnlyDenom(price.info) })) + .reduce((acc, cur) => { + acc[cur.info] = parseFloat(cur.amount); + return acc; + }, {}); + } + let tempData = {}; + for (const key in volInfo) { + if (key === "timestamp" || key === "txheight") continue; + if (Object.keys(tempData).includes("volume_price")) { + tempData["volume_price"] += volInfo[key] * prices[key]; + } else { + tempData["timestamp"] = volInfo["timestamp"]; + tempData["volume_price"] = 0; + } + } + const indexOf = finalArray.findIndex( + (data) => new Date(data.timestamp).toISOString() === new Date(tempData["timestamp"]).toISOString() + ); + if (indexOf === -1) finalArray.push(tempData); + else { + finalArray[indexOf] = { + ...finalArray[indexOf], + volume_price: finalArray[indexOf].volume_price + tempData["volume_price"] + }; + } + heightCount++; + } + console.log("temp data: ", finalArray); + // let swapTokenMap = []; + // const baseVolume = 1000000000; + // for (let i = 0; i < swapOps.length; i++) { + // const indexOf = swapTokenMap.findIndex( + // (swapMap) => new Date(swapMap.timestamp).toISOString() === new Date(swapOps[i].timestamp).toISOString() + // ); + // console.log("index of: ", indexOf); + // if (indexOf === -1) { + // swapTokenMap.push({ + // timestamp: swapOps[i].timestamp, + // tokenData: uniqueInfos.map((info) => { + // if (parseAssetInfoOnlyDenom(info) === swapOps[i].offerDenom) { + // return { denom: swapOps[i].offerDenom, amount: swapOps[i].offerAmount }; + // } + // if (parseAssetInfoOnlyDenom(info) === swapOps[i].askDenom) { + // return { denom: swapOps[i].askDenom, amount: swapOps[i].returnAmount }; + // } + // return { denom: parseAssetInfoOnlyDenom(info), amount: 0 }; + // }) + // }); + // } else { + // swapTokenMap[indexOf] = { + // ...swapTokenMap[indexOf], + // tokenData: swapTokenMap[indexOf].tokenData.map((tokenData) => { + // if (tokenData.denom === swapOps[i].offerDenom) + // return { ...tokenData, amount: tokenData.amount + swapOps[i].offerAmount }; + // if (tokenData.denom === swapOps[i].askDenom) + // return { ...tokenData, amount: tokenData.amount + swapOps[i].returnAmount }; + // return tokenData; + // }) + // }; + // } + // } + // console.log( + // swapTokenMap.map((tokenMap) => ({ + // ...tokenMap, + // tokenData: tokenMap.tokenData.reduce((acc, item) => { + // acc[item.denom] = item.amount; + // return acc; + // }, {}) + // })) + // ); // const newData = calculatePrefixSum( // 100000000000, // result.map((res) => ({ denom: "", amount: res.liquidity })) diff --git a/packages/oraidex-sync/src/tx-parsing.ts b/packages/oraidex-sync/src/tx-parsing.ts index 94a412b1..ff06d398 100644 --- a/packages/oraidex-sync/src/tx-parsing.ts +++ b/packages/oraidex-sync/src/tx-parsing.ts @@ -15,10 +15,11 @@ import { ProvideLiquidityOperationData, SwapOperationData, TxAnlysisResult, + VolumeInfo, WithdrawLiquidityOperationData } from "./types"; import { Log } from "@cosmjs/stargate/build/logs"; -import { calculatePrefixSum, parseAssetInfo, parseAssetInfoOnlyDenom } from "./helper"; +import { calculatePrefixSum, isoToTimestampNumber, parseAssetInfo, parseAssetInfoOnlyDenom } from "./helper"; function parseWasmEvents(events: readonly Event[]): (readonly Attribute[])[] { return events.filter((event) => event.type === "wasm").map((event) => event.attributes); @@ -205,7 +206,11 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { transactions.push(tx); const msgExecuteContracts = parseTxToMsgExecuteContractMsgs(tx); const msgs = parseExecuteContractToOraidexMsgs(msgExecuteContracts); - const basicTxData: BasicTxData = { timestamp: tx.timestamp, txhash: tx.hash, txheight: tx.height }; + const basicTxData: BasicTxData = { + timestamp: isoToTimestampNumber(tx.timestamp), + txhash: tx.hash, + txheight: tx.height + }; for (let msg of msgs) { const sender = msg.sender; const wasmAttributes = parseWasmEvents(msg.logs.events); @@ -216,9 +221,27 @@ function parseTxs(txs: Tx[]): TxAnlysisResult { accountTxs.push({ txhash: basicTxData.txhash, accountAddress: sender }); } } + let volumeInfos: VolumeInfo[] = []; + swapOpsData.forEach((op) => { + volumeInfos.push( + { + denom: op.offerDenom, + timestamp: op.timestamp, + txheight: op.txheight, + volume: op.offerAmount + } as VolumeInfo, + { + denom: op.askDenom, + timestamp: op.timestamp, + txheight: op.txheight, + volume: op.returnAmount + } as VolumeInfo + ); + }); return { // transactions: txs, swapOpsData, + volumeInfos, accountTxs, provideLiquidityOpsData, withdrawLiquidityOpsData diff --git a/packages/oraidex-sync/src/types.ts b/packages/oraidex-sync/src/types.ts index 2784762e..496feca6 100644 --- a/packages/oraidex-sync/src/types.ts +++ b/packages/oraidex-sync/src/types.ts @@ -13,7 +13,7 @@ export type AssetData = { }; export type BasicTxData = { - timestamp: string; + timestamp: number; txhash: string; txheight: number; }; @@ -49,7 +49,7 @@ export type PairInfoData = { export type PriceInfo = { txheight: number; - timestamp: string; + timestamp: number; assetInfo: string; price: number; }; @@ -77,6 +77,7 @@ export type WithdrawLiquidityOperationData = ProvideLiquidityOperationData; export type TxAnlysisResult = { // transactions: Tx[]; swapOpsData: SwapOperationData[]; + volumeInfos: VolumeInfo[]; accountTxs: AccountTx[]; provideLiquidityOpsData: ProvideLiquidityOperationData[]; withdrawLiquidityOpsData: WithdrawLiquidityOperationData[]; @@ -152,3 +153,10 @@ export type TotalLiquidity = { liquidity: number; height: number; }; + +export type VolumeInfo = { + denom: string; + timestamp: number; + txheight: number; + volume: number; +}; diff --git a/packages/oraidex-sync/tests/db.spec.ts b/packages/oraidex-sync/tests/db.spec.ts index a5c7deee..049918c2 100644 --- a/packages/oraidex-sync/tests/db.spec.ts +++ b/packages/oraidex-sync/tests/db.spec.ts @@ -1,4 +1,5 @@ import { DuckDb } from "../src/db"; +import { isoToTimestampNumber } from "../src/helper"; describe("test-duckdb", () => { let duckDb: DuckDb; @@ -26,7 +27,7 @@ describe("test-duckdb", () => { returnAmount: 100, spreadAmount: 0, taxAmount: 0, - timestamp: new Date(1689610068000).toISOString(), + timestamp: 168961006800 / 1000, txhash: "foo", txheight: 1 }, @@ -38,7 +39,7 @@ describe("test-duckdb", () => { returnAmount: 1, spreadAmount: 0, taxAmount: 0, - timestamp: new Date(1589610068000).toISOString(), + timestamp: 1589610068000 / 1000, txhash: "foo", txheight: 1 }, @@ -50,7 +51,7 @@ describe("test-duckdb", () => { returnAmount: 1, spreadAmount: 0, taxAmount: 0, - timestamp: new Date(1589610068000).toISOString(), + timestamp: 1589610068000 / 1000, txhash: "foo", txheight: 1 }, @@ -62,7 +63,7 @@ describe("test-duckdb", () => { returnAmount: 1, spreadAmount: 0, taxAmount: 0, - timestamp: new Date(1589610068000).toISOString(), + timestamp: 1589610068000 / 1000, txhash: "foo", txheight: 1 } @@ -85,7 +86,7 @@ describe("test-duckdb", () => { returnAmount: 100, spreadAmount: 0, taxAmount: 0, - timestamp: new Date("2023-07-17T16:07:48.000Z").toISOString(), + timestamp: new Date("2023-07-17T16:07:48.000Z").getTime() / 1000, txhash: "foo", txheight: 1 }, @@ -97,7 +98,7 @@ describe("test-duckdb", () => { returnAmount: 1, spreadAmount: 0, taxAmount: 0, - timestamp: new Date("2023-07-16T16:07:48.000Z").toISOString(), + timestamp: new Date("2023-07-16T16:07:48.000Z").getTime() / 1000, txhash: "foo", txheight: 1 }, @@ -109,7 +110,7 @@ describe("test-duckdb", () => { returnAmount: 10000, spreadAmount: 0, taxAmount: 0, - timestamp: new Date(1389610068000).toISOString(), + timestamp: new Date(1389610068000).getTime() / 1000, txhash: "foo", txheight: 1 }, @@ -121,7 +122,7 @@ describe("test-duckdb", () => { returnAmount: 10000, spreadAmount: 0, taxAmount: 0, - timestamp: new Date(1389610068000).toISOString(), + timestamp: new Date(1389610068000).getTime() / 1000, txhash: "foo", txheight: 1 } @@ -129,8 +130,8 @@ describe("test-duckdb", () => { let queryResult = await duckDb.queryAllVolumeRange( "orai", "atom", - "2023-07-16T16:07:48.000Z", - "2023-07-17T16:07:48.000Z" + isoToTimestampNumber("2023-07-16T16:07:48.000Z"), + isoToTimestampNumber("2023-07-17T16:07:48.000Z") ); console.log("result: ", queryResult); expect(queryResult.volume["orai"]).toEqual(110); @@ -139,8 +140,8 @@ describe("test-duckdb", () => { queryResult = await duckDb.queryAllVolumeRange( "orai", "atom", - "2023-07-16T16:07:48.000Z", - "2023-07-17T16:07:48.000Z" + isoToTimestampNumber("2023-07-16T16:07:48.000Z"), + isoToTimestampNumber("2023-07-17T16:07:48.000Z") ); expect(queryResult.volume["orai"]).toEqual(110); expect(queryResult.volume["atom"]).toEqual(10001); @@ -155,7 +156,7 @@ describe("test-duckdb", () => { duckDb.insertLpOps([ { txhash: "foo", - timestamp: new Date().toISOString(), + timestamp: new Date().getTime() / 1000, firstTokenAmount: "abcd" as any, firstTokenLp: 0, firstTokenDenom: "orai", @@ -175,7 +176,7 @@ describe("test-duckdb", () => { duckDb = await DuckDb.create(":memory:"); await Promise.all([duckDb.createHeightSnapshot(), duckDb.createLiquidityOpsTable(), duckDb.createSwapOpsTable()]); // act & test - const newDate = new Date(1689610068000).toISOString(); + const newDate = 1689610068000 / 1000; await duckDb.insertLpOps([ { firstTokenAmount: 1, @@ -192,7 +193,7 @@ describe("test-duckdb", () => { } ]); let queryResult = await duckDb.queryLpOps(); - queryResult[0].timestamp = new Date(queryResult[0].timestamp).toISOString(); + queryResult[0].timestamp = queryResult[0].timestamp; expect(queryResult[0]).toEqual({ txhash: "foo", timestamp: newDate, diff --git a/packages/oraidex-sync/tests/helper.spec.ts b/packages/oraidex-sync/tests/helper.spec.ts index 4981f1a3..93d16d61 100644 --- a/packages/oraidex-sync/tests/helper.spec.ts +++ b/packages/oraidex-sync/tests/helper.spec.ts @@ -246,7 +246,7 @@ describe("test-helper", () => { native_token: { denom: osmosisIbcDenom } } ], - symbols: ["ORAI", "OSMOSIS"] + symbols: ["ORAI", "OSMO"] }, { asset_infos: [{ token: { contract_addr: milkyCw20Address } }, { token: { contract_addr: usdtCw20Address } }], From 02cce04c6967fe0829da403eadf263742313343e Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 20 Jul 2023 18:01:47 +0700 Subject: [PATCH 75/75] added volume chart timeframe query --- packages/oraidex-server/src/index.ts | 68 ++++++++++++++++++++++++++-- packages/oraidex-sync/src/db.ts | 20 ++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/packages/oraidex-server/src/index.ts b/packages/oraidex-server/src/index.ts index c03fc73c..447831ee 100644 --- a/packages/oraidex-server/src/index.ts +++ b/packages/oraidex-server/src/index.ts @@ -10,7 +10,9 @@ import { OraiDexSync, simulateSwapPrice, getPoolInfos, - calculatePrefixSum + calculatePrefixSum, + uniqueInfos, + simulateSwapPriceWithUsdt } from "@oraichain/oraidex-sync"; import cors from "cors"; import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; @@ -67,11 +69,11 @@ app.get("/tickers", async (req, res) => { const baseIndex = 0; const targetIndex = 1; const latestTimestamp = endTime ? parseInt(endTime as string) : await duckDb.queryLatestTimestampSwapOps(); - const now = new Date(latestTimestamp); - const then = getDate24hBeforeNow(now).toISOString(); + const then = getDate24hBeforeNow(new Date(latestTimestamp * 1000)).getTime() / 1000; + console.log(latestTimestamp, then); const baseInfo = parseAssetInfoOnlyDenom(pair.asset_infos[baseIndex]); const targetInfo = parseAssetInfoOnlyDenom(pair.asset_infos[targetIndex]); - const volume = await duckDb.queryAllVolumeRange(baseInfo, targetInfo, then, now.toISOString()); + const volume = await duckDb.queryAllVolumeRange(baseInfo, targetInfo, then, latestTimestamp); let tickerInfo: TickerInfo = { ticker_id: tickerId, base_currency: symbols[baseIndex], @@ -105,6 +107,64 @@ app.get("/tickers", async (req, res) => { } }); +// TODO: refactor this and add unit tests +app.get("/volume/v2/historical/chart", async (req, res) => { + const { startTime, endTime, tf } = req.query; + const timeFrame = parseInt(tf as string); + const volumeInfos = await duckDb.pivotVolumeRange(parseInt(startTime as string), parseInt(endTime as string)); + const cosmwasmClient = await CosmWasmClient.connect(process.env.RPC_URL); + let finalArray = []; + let prices; + let heightCount = 0; + for (let i = 0; i < volumeInfos.length; i++) { + const volInfo = volumeInfos[i]; + cosmwasmClient.setQueryClientWithHeight(volInfo.txheight); + const router = new OraiswapRouterQueryClient( + cosmwasmClient, + process.env.ROUTER_CONTRACT_ADDRESS || "orai1j0r67r9k8t34pnhy00x3ftuxuwg0r6r4p8p6rrc8az0ednzr8y9s3sj2sf" + ); + if (heightCount % 1000 === 0) { + // prevent simulating too many times. TODO: calculate this using pool data from + prices = (await Promise.all(uniqueInfos.map((info) => simulateSwapPriceWithUsdt(info, router)))) + .map((price) => ({ ...price, info: parseAssetInfoOnlyDenom(price.info) })) + .reduce((acc, cur) => { + acc[cur.info] = parseFloat(cur.amount); + return acc; + }, {}); + } + let tempData = {}; + for (const key in volInfo) { + if (key === "timestamp" || key === "txheight") continue; + if (Object.keys(tempData).includes("volume_price")) { + tempData["volume_price"] += volInfo[key] * prices[key]; + } else { + tempData["timestamp"] = volInfo["timestamp"]; + tempData["volume_price"] = 0; + } + } + const indexOf = finalArray.findIndex((data) => data.timestamp === tempData["timestamp"]); + if (indexOf === -1) finalArray.push(tempData); + else { + finalArray[indexOf] = { + ...finalArray[indexOf], + volume_price: finalArray[indexOf].volume_price + tempData["volume_price"] + }; + } + heightCount++; + } + let finalFinalArray = []; + for (let data of finalArray) { + let time = Math.floor(data.timestamp / timeFrame); + let index = finalFinalArray.findIndex((data) => data.timestamp === time); + if (index === -1) { + finalFinalArray.push({ timestamp: time, volume_price: data.volume_price }); + } else { + finalFinalArray[index].volume_price += data.volume_price; + } + } + res.status(200).send(finalFinalArray); +}); + // app.get("/liquidity/v2/historical/chart", async (req, res) => { // let { start, end, tf } = req.query; // // start, end time is timestamp in ms. tf is the in sec diff --git a/packages/oraidex-sync/src/db.ts b/packages/oraidex-sync/src/db.ts index bc6e3cdd..1c14e324 100644 --- a/packages/oraidex-sync/src/db.ts +++ b/packages/oraidex-sync/src/db.ts @@ -291,4 +291,24 @@ export class DuckDb { async insertVolumeInfo(volumeInfos: VolumeInfo[]) { await this.insertBulkData(volumeInfos, "volume_info"); } + + async pivotVolumeRange(startTime: number, endTime: number) { + let volumeInfos = await this.conn.all( + ` + pivot (select * from volume_info + where timestamp >= ${startTime} + and timestamp <= ${endTime} + order by timestamp) + on denom + using sum(volume) + group by timestamp, txheight + order by timestamp` + ); + for (let volInfo of volumeInfos) { + for (const key in volInfo) { + if (volInfo[key] === null) volInfo[key] = 0; + } + } + return volumeInfos; + } }

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 Wzb??#HRTWS_FjXXf;GU(gX%xueP~Aj diff --git a/packages/contracts-build/data/oraiswap_pair.wasm b/packages/contracts-build/data/oraiswap_pair.wasm index e3a901be47d7ff2f4915f4c52f77279b3bc78cca..1a6e2f9475c5f832567630b7010ede7795d70d4d 100644 GIT binary patch delta 66527 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 ax9`jmnJH?_MNd75Ur+p!@%!yo_x}N)qka+q delta 65019 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 Y> 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 delta 89510 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