├── .gitattributes ├── .github ├── release.t.md └── workflows │ ├── lint.yml │ ├── scan.yml │ ├── ship.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.toml ├── LICENSE ├── README.md ├── book ├── README.md ├── book.toml └── src │ ├── SUMMARY.md │ ├── dev │ ├── build.md │ └── proxy.md │ ├── index.md │ └── setup │ ├── config.md │ ├── installation.md │ └── spin.md ├── e2e ├── Gemfile ├── install.rb ├── lib.js ├── logger.rb ├── main.rb └── os.rb ├── install ├── linux.sh ├── mac.sh └── windows.ps1 ├── package.json ├── rollup.config.js ├── rollup.test.js ├── scripts ├── fix-corrupt.js ├── migrate.js ├── post_alpha_migration.js ├── refund.js └── refund_orders.js ├── src ├── api │ ├── index.ts │ └── routes.ts ├── commands │ ├── init.ts │ ├── orders.ts │ └── upgrade.ts ├── declarations.d.ts ├── index.ts ├── queries │ ├── genesis.gql │ ├── tx.gql │ └── txs.gql ├── utils │ ├── arweave.ts │ ├── config.ts │ ├── console.ts │ ├── constants.yml │ ├── database.ts │ ├── eth.ts │ ├── gql.ts │ ├── logger.ts │ ├── swap.ts │ └── tree.ts └── workflows │ ├── bootstrap.ts │ ├── cancel.ts │ ├── genesis.ts │ ├── match.ts │ └── swap.ts ├── test ├── README.md ├── api.test.ts ├── commands.test.ts ├── config.test.ts ├── database.test.ts └── logger.test.ts ├── tsconfig.json ├── verto.config.example.json ├── verto.config.schema.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/release.t.md: -------------------------------------------------------------------------------- 1 | ### Changes 2 | 3 | {{ CHANGELOG }} 4 | 5 | ### Install / Upgrade 6 | 7 | **LINUX** 8 | 9 | ```sh 10 | curl -fsSL https://verto.exchange/i/linux | sh 11 | ``` 12 | 13 | **MACOS** 14 | 15 | ```sh 16 | curl -fsSL https://verto.exchange/i/mac | sh 17 | ``` 18 | 19 | **WINDOWS** 20 | 21 | ```ps1 22 | iwr https://verto.exchange/i/windows | iex 23 | ``` 24 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Setup repo 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 14.x 18 | 19 | - name: Install dependencies 20 | run: | 21 | npm i -g yarn 22 | yarn 23 | 24 | - name: Check formatting 25 | run: yarn fmt:check 26 | -------------------------------------------------------------------------------- /.github/workflows/scan.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * 0" 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 2 19 | 20 | - name: Checkout HEAD^2 on Pull Request 21 | if: ${{ github.event_name == 'pull_request' }} 22 | run: git checkout HEAD^2 23 | 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v1 26 | with: 27 | languages: javascript 28 | 29 | - name: Perform CodeQL Analysis 30 | uses: github/codeql-action/analyze@v1 31 | -------------------------------------------------------------------------------- /.github/workflows/ship.yml: -------------------------------------------------------------------------------- 1 | name: Ship 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | ship: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macos-latest] 12 | 13 | steps: 14 | - name: Setup repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup node 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 14.x 21 | 22 | - name: Setup Deno 23 | if: startsWith(matrix.os, 'ubuntu') 24 | uses: denolib/setup-deno@v2 25 | with: 26 | deno-version: v1.3.2 27 | 28 | - run: npm publish --access public 29 | env: 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | 32 | - name: Get Version 33 | id: version 34 | run: echo "::set-output name=tag::$(node -p "require('./package.json').version")" 35 | 36 | - name: Generate Release Notes 37 | if: startsWith(matrix.os, 'ubuntu') 38 | run: deno run -A https://deno.land/x/prlog@0.3.1/prlog.ts useverto/trading-post -v ${{ steps.version.outputs.tag }} -t .github/release.t.md -o release.md --auth ${{ secrets.GITHUB_TOKEN }} 39 | 40 | - name: Upload Release Notes 41 | uses: actions/upload-artifact@v2 42 | if: startsWith(matrix.os, 'ubuntu') 43 | with: 44 | name: notes 45 | path: release.md 46 | if-no-files-found: error 47 | 48 | - name: Install dependencies 49 | run: | 50 | npm i -g yarn 51 | yarn 52 | 53 | - name: Package executables 54 | run: yarn pkg 55 | 56 | - name: Zip linux release 57 | if: startsWith(matrix.os, 'ubuntu') 58 | run: | 59 | sqlite3=$(find . -name node_sqlite3.node) 60 | cp $sqlite3 ./node_sqlite3.node 61 | zip -r verto-x64-linux.zip verto README.md LICENSE node_sqlite3.node 62 | 63 | - name: Zip mac release 64 | if: startsWith(matrix.os, 'mac') 65 | run: | 66 | sqlite3=$(find . -name node_sqlite3.node) 67 | cp $sqlite3 ./node_sqlite3.node 68 | zip -r verto-x64-macos.zip verto README.md LICENSE node_sqlite3.node 69 | 70 | - name: Zip windows release 71 | if: startsWith(matrix.os, 'windows') 72 | run: | 73 | $sqlite3=$(gci -filter "node_sqlite3.node" -af -s -name) 74 | Compress-Archive -CompressionLevel Optimal -Force -Path verto.exe, README.md, LICENSE, $sqlite3 -DestinationPath verto-x64-windows.zip 75 | 76 | - name: Upload release artifacts 77 | uses: actions/upload-artifact@v2 78 | with: 79 | name: release 80 | path: | 81 | verto-x64-linux.zip 82 | verto-x64-macos.zip 83 | verto-x64-windows.zip 84 | 85 | - name: Download artifacts 86 | uses: actions/download-artifact@v2 87 | 88 | - name: Release 89 | uses: ncipollo/release-action@v1 90 | with: 91 | tag: ${{ steps.version.outputs.tag }} 92 | name: ${{ steps.version.outputs.tag }} 93 | draft: true 94 | prerelease: false 95 | allowUpdates: true 96 | replacesArtifacts: true 97 | artifacts: "release/*" 98 | bodyFile: "notes/release.md" 99 | token: ${{ secrets.GITHUB_TOKEN }} 100 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, windows-latest, macos-latest] 15 | 16 | steps: 17 | - name: Setup repo 18 | uses: actions/checkout@v2 19 | 20 | - name: Setup ruby 21 | uses: actions/setup-ruby@v1 22 | with: 23 | ruby-version: "2.7" 24 | 25 | - name: Setup node 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: 14.x 29 | 30 | - name: Install dependencies 31 | run: | 32 | npm i -g yarn 33 | yarn 34 | 35 | - name: Build source 36 | run: yarn build 37 | 38 | - name: Run Unit Tests 39 | run: yarn test 40 | 41 | - name: Package executables 42 | run: yarn pkg 43 | 44 | - name: Run Integration Tests 45 | run: yarn test:integration 46 | env: 47 | KEYFILE: ${{ secrets.KEYFILE }} 48 | 49 | - name: Prepare linux build 50 | if: startsWith(matrix.os, 'ubuntu') 51 | run: | 52 | sqlite3=$(find . -name node_sqlite3.node) 53 | cp $sqlite3 ./node_sqlite3.node 54 | 55 | - name: Prepare mac build 56 | if: startsWith(matrix.os, 'mac') 57 | run: | 58 | sqlite3=$(find . -name node_sqlite3.node) 59 | cp $sqlite3 ./node_sqlite3.node 60 | 61 | - name: Prepare windows build 62 | if: startsWith(matrix.os, 'windows') 63 | run: | 64 | $sqlite3=$(gci -filter "node_sqlite3.node" -af -s -name) 65 | cp $sqlite3 node_sqlite3.node 66 | 67 | - name: Upload release artifacts 68 | uses: actions/upload-artifact@v2 69 | with: 70 | name: ${{ matrix.os }} 71 | path: | 72 | verto.exe 73 | verto 74 | README.md 75 | LICENSE 76 | node_sqlite3.node 77 | 78 | bench: 79 | name: Benchmarks 80 | 81 | runs-on: ubuntu-latest 82 | 83 | steps: 84 | - name: Setup repo 85 | uses: actions/checkout@v2 86 | 87 | - name: Setup node 88 | uses: actions/setup-node@v1 89 | with: 90 | node-version: 14.x 91 | 92 | - name: Install dependencies 93 | run: | 94 | npm i -g yarn 95 | yarn 96 | 97 | - name: Install hyperfine 98 | run: | 99 | wget https://github.com/sharkdp/hyperfine/releases/download/v1.10.0/hyperfine_1.10.0_amd64.deb 100 | sudo dpkg -i hyperfine_1.10.0_amd64.deb 101 | 102 | - name: Run benchmarks 103 | run: | 104 | yarn pkg 105 | hyperfine 'node ./dist/verto.js -h' './verto -h' 'node ./dist/verto.js orders' './verto orders' -i -w 10 -s full --export-markdown bench.md 106 | 107 | - name: Get Pull Request 108 | uses: jwalton/gh-find-current-pr@v1 109 | id: finder 110 | with: 111 | github-token: ${{ secrets.GITHUB_TOKEN }} 112 | 113 | - name: Comment Benchmarks 114 | uses: marocchino/sticky-pull-request-comment@v1 115 | with: 116 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 117 | number: ${{ steps.finder.outputs.pr }} 118 | path: bench.md 119 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # verto production bundles 2 | dist 3 | 4 | # logs 5 | logs 6 | *.log 7 | 8 | # nodejs modules 9 | node_modules 10 | 11 | # arweave keyfiles 12 | arweave.json 13 | arweave-keyfile.json 14 | privatekey 15 | 16 | # verto config files 17 | config.json 18 | verto.config.json 19 | 20 | # test build dir 21 | build 22 | 23 | # verto production binaries 24 | verto 25 | verto.exe 26 | vertolinux 27 | verto-mac 28 | verto-win.exe 29 | verto.zip 30 | node_sqlite3.node 31 | 32 | # test generated artifacts 33 | test_artifacts/ 34 | 35 | # enviornment variables 36 | .env 37 | 38 | # verto db file 39 | db 40 | db.db -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # .prettierignore 2 | build 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc.toml: -------------------------------------------------------------------------------- 1 | # .prettierrc.toml 2 | trailingComma = "es5" 3 | tabWidth = 2 4 | semi = true 5 | singleQuote = false 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 The Verto Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Verto logo (light version) 4 | 5 | 6 |

Verto Trading Posts

7 | 8 |

9 | Everything needed to become part of the Verto Exchange Network 10 |

11 | 12 |

13 | Fancy CI badge 14 |

15 | 16 |

17 | 18 | ## About 19 | 20 | This repository contains all of the necessary code to start a trading post of your own. 21 | 22 | You can access the code for our frontend [here](https://github.com/useverto/verto). 23 | 24 | > Important Notice: Verto is in its Alpha stage. If you have a suggestion, idea, or find a bug, please report it! The Verto team will not be held accountable for any funds lost. 25 | 26 | ## Guide 27 | 28 | Deploying your own trading post is extremely easy! In this short guide, we'll show you exactly what to do. 29 | 30 | ### Book 31 | 32 | The trading post book is a detailed guide to setting up a trading post. You can find it [here](./book) 33 | 34 | ### Token Staking 35 | 36 | To ensure the integrity of the Verto Trading Post Network, you'll need to purchase and stake `VRT` tokens. The more tokens you stake, the higher your reputation will be. If a trading post begins acting malicious, the Verto DAO will be able to vote to slash that trading post's stake. 37 | 38 | You can purchase `VRT` with `AR` on [Verto](https://verto.exchange/trade)! 39 | 40 | To stake, you need to lock tokens in the vault on our [Community](https://community.xyz/#aALHIrtzQzy88AhH9uVGxr2GrdSngu2x1CYbyi50JaA/vault). 41 | 42 | > Note: You must use configure the trading post to use the same wallet as the one you have staked currency in. 43 | 44 | #### Reputation 45 | 46 | As mentioned above, you'll need to stake `VRT` tokens to be a trading post. A trading post's reputation is determined by the following factors: 47 | 48 | - Amount of stake (weighted at 50%) 49 | - A medium that the DAO can hold the trading post accountable for 50 | - Amount of time staked (weighted at 33%) 51 | - Provides proof of dedication to the platform 52 | - Balance (weighted at 17%) 53 | - Provides a rough estimation for the popularity of the trading post 54 | 55 | #### Conclusion 56 | 57 | After you've started the trading post, you might want to [set up a reverse proxy](./book/dev/PROXY.md) for the trading post API. 58 | 59 | And that's it! Your trading post will proceed to send a genesis transaction to the exchange wallet, which will officially list it on the [gallery](https://verto.exchange/gallery)! 60 | 61 | If you have any questions or need to talk to someone, join our [Discord](https://discord.gg/RnWbc8Y)! 62 | 63 | ## Special Thanks 64 | 65 | - [Sam Williams](https://github.com/samcamwilliams) 66 | - [Cedrik Boudreau](https://github.com/cedriking) 67 | - [Aidan O'Kelly](https://github.com/aidanok) 68 | 69 | ## License 70 | 71 | The code contained within this repository is licensed under the MIT license. 72 | See [`./LICENSE`](./LICENSE) for more information. 73 | -------------------------------------------------------------------------------- /book/README.md: -------------------------------------------------------------------------------- 1 | # Verto Trading Post Book 2 | 3 | To build the book, you will first need to [install mdBook via cargo][install-mdbook]: 4 | 5 | ```sh 6 | cargo install mdbook 7 | ``` 8 | 9 | You can then serve the documentation locally by calling the [`serve` command][mdbook-serve] 10 | from the `book` directory: 11 | 12 | ```sh 13 | mdbook serve 14 | ``` 15 | 16 | [install-mdbook]: https://rust-lang-nursery.github.io/mdBook/cli/cli-tool.html#install-cratesio-version 17 | [mdbook-serve]: https://rust-lang-nursery.github.io/mdBook/cli/serve.html 18 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "Verto Trading Post Book" 3 | authors = ["Verto Authors"] 4 | description = "Documentation to run a trading post on the Verto Protocol" 5 | multilingual = false 6 | src = "src" 7 | 8 | [build] 9 | build-dir = "build" 10 | 11 | [output.html] 12 | mathjax-support = true -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](index.md) 4 | 5 | - [Getting Started](./setup/config.md) 6 | - [Installation](./setup/installation.md) 7 | - [Setup](./setup/config.md) 8 | - [Running](./setup/spin.md) 9 | - [Advanced](./dev/build.md) 10 | - [Build from source](./dev/build.md) 11 | - [Proxy](./dev/proxy.md) 12 | -------------------------------------------------------------------------------- /book/src/dev/build.md: -------------------------------------------------------------------------------- 1 | ## Build from source 2 | 3 | > It is recommended to use pre-built production binaries when running a trading post. 4 | 5 | In order to build a trading post from source, make sure you have `git` and `node` installed on your machine. 6 | 7 | Clone the repo and make it your working directory 8 | 9 | ```shell script 10 | git clone https://github.com/useverto/trading-post 11 | cd trading-post 12 | ``` 13 | 14 | Using your favourite package manager, download the required dependencies 15 | 16 | ```shell script 17 | yarn 18 | ``` 19 | 20 | Now, it's time to build the trading post! It is as simple as: 21 | 22 | ```shell script 23 | yarn prod 24 | ``` 25 | 26 | Awesome! You've successfully built the trading post! 27 | It is now avaliable at `./dist/verto.js` 28 | 29 | > Make sure to create a `verto.config.json` for your trading post. See [Configuration](../setup/config.md) for more information. 30 | 31 | and finally start the trading post! 🙂 32 | 33 | ```shell script 34 | node ./dist/verto.js --key-file /path/to/your/keyfile.json 35 | ``` 36 | 37 | Now, you can sit back and relax while the trading post greets you with some colourful logs. 38 | -------------------------------------------------------------------------------- /book/src/dev/proxy.md: -------------------------------------------------------------------------------- 1 | While hosting a trading post, you might need to set up a reverse proxy. 2 | 3 | ## Nginx 4 | 5 | You can find the official docs for setting up a reverse proxy at https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/ 6 | 7 | Install `nginx` on a Ubuntu server 8 | 9 | ```shell script 10 | sudo apt-get update 11 | sudo apt-get install nginx 12 | ``` 13 | 14 | > You can confirm your nginx installation using `nginx -v` 15 | 16 | **Adding your domain** 17 | 18 | First, create a Nginx virtual host configuration using the follwing command: 19 | 20 | ```shell script 21 | sudo touch /etc/nginx/sites-available/YOUR-DOMAIN 22 | ``` 23 | 24 | > Be sure to replace YOUR-DOMAIN with the domain you plan to associate with the trading post. 25 | 26 | **Create nginx configuration** 27 | 28 | Next, we setup our nginx configuration by editing the file that we just created. 29 | 30 | ```shell script 31 | sudo nano /etc/nginx/sites-available/YOUR-DOMAIN 32 | ``` 33 | 34 | > You can either use `vim` or `nano` as your text editor 35 | 36 | You can now paste the following configuration: 37 | 38 | ```nginx 39 | server { 40 | listen 80; 41 | listen [::]:80; 42 | server_name YOUR-DOMAIN; 43 | 44 | location ^~ /.well-known/acme-challenge { 45 | default_type text/plain; 46 | root /path/to/letsencrypt/challenge; 47 | } 48 | 49 | location / { 50 | return 301 https://$host$request_uri; 51 | } 52 | } 53 | server { 54 | listen 443 ssl http2; 55 | listen [::]:443 ssl http2; 56 | server_name YOUR-DOMAIN; 57 | 58 | ssl_certificate /path/to/cert.pem; 59 | ssl_certificate_key /path/to/key.pem; 60 | ssl_trusted_certificate /path/to/ca.pem; 61 | ssl_dhparam /path/to/dhparams.pem; 62 | 63 | ssl_protocols TLSv1.2 TLSv1.3; 64 | ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; 65 | ssl_ecdh_curve prime256v1:secp384r1; 66 | ssl_prefer_server_ciphers on; 67 | 68 | add_header Strict-Transport-Security "max-age=63072000; preload;" always; 69 | 70 | ssl_stapling on; 71 | ssl_stapling_verify on; 72 | 73 | resolver 1.1.1.1; 74 | 75 | ssl_session_timeout 24h; 76 | ssl_session_cache shared:SSL:50m; 77 | ssl_session_tickets off; 78 | 79 | access_log /var/log/nginx/access.log; 80 | 81 | location / { 82 | proxy_set_header Host $host; 83 | proxy_set_header X-Real-IP $remote_addr; 84 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 85 | proxy_set_header X-Forwarded-Proto $scheme; 86 | proxy_pass http://localhost:8080; 87 | # change this to the port where the trading post is running 88 | proxy_read_timeout 90; 89 | } 90 | } 91 | ``` 92 | 93 | > SSL is compulsory otherwise CORS for the trading post API will be blocked. You can easily generate SSL certificates using Let's Encrypt and specify the certificate file and key in the above configuration 94 | 95 | **Be sure to replace YOUR-DOMAIN with your actual domain and make sure your trading post is running at port 8080.** 96 | 97 | Save the file and proceed to the final step. 98 | 99 | **Start nginx** 100 | 101 | Before starting nginx, we will need to link the file in the `sites-available` folder to a location within the `sites-enabled` folder. 102 | 103 | Again, change YOUR-DOMAIN here with the actual name of the file you created earlier. 104 | 105 | ```shell script 106 | ln -s /etc/nginx/sites-avaialable/YOUR-DOMAIN /etc/nginx/sites-enabled/YOUR-DOMAIN.conf 107 | ``` 108 | 109 | Let’s now test the configuration file. 110 | 111 | ```shell script 112 | sudo nginx -t 113 | ``` 114 | 115 | If the test is successful, you’ll see this output: 116 | 117 | ```bash 118 | nginx: the configuration file /etc/nginx/nginx.conf syntax is ok 119 | nginx: configuration file /etc/nginx/nginx.conf test is successful 120 | ``` 121 | 122 | Now that we know it’s going to work as expected, issue the command to restart the Nginx service 123 | 124 | ```shell script 125 | sudo systemctl restart nginx 126 | 127 | # OR # 128 | 129 | sudo service nginx restart 130 | ``` 131 | 132 | Both commands perform the same task, simply preference decides your method here. 133 | 134 | Congratulations! You should now be able to launch your trading post (if it wasn’t running already) and visit `YOUR-DOMAIN` in a browser, assuming the DNS is correct. :smile: 135 | -------------------------------------------------------------------------------- /book/src/index.md: -------------------------------------------------------------------------------- 1 | # Verto Trading Post 2 | 3 | Verto is a decentralized token exchange protocol built on [Arweave](https://arweave.org) powered by a network of trading posts. The following documentation contains the information necessary to start up a trading post of your own. 4 | 5 | ## Prerequisites 6 | 7 | Before initializing your trading post, ensure that you have [VRT](https://community.xyz/#usjm4PCxUd5mtaon7zc97-dt-3qf67yPyqgzLnLqk5A/tokens) staked in your wallet. Without staked VRT, users will be unable to discover your trading post on the exchange. 8 | 9 | - [Source code](https://github.com/useverto/trading-post) 10 | - [Issues](https://github.com/useverto/trading-post/issues) 11 | - [Discord Chat](https://verto.exchange/chat) 12 | -------------------------------------------------------------------------------- /book/src/setup/config.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | Before deploying a trading post, you'll want to properly configure the system. You'll also need to drag & drop your keyfile to the root of your trading post and make sure the name of the file is `arweave.json`. 4 | 5 | ## verto.config.json 6 | 7 | The `verto.config.json` file is where the majority of your configuration will lie. You'll need to create this file before you can run a trading post. As seen in the config.example.json file, it must contain the following information: 8 | 9 | ```json 10 | { 11 | "genesis": { 12 | "blockedTokens": [], 13 | "chain": { "ETH": "0x70dd63799560097E7807Ea94BA0CE5A85C1feAD8" }, 14 | "tradeFee": 0.01, 15 | "publicURL": "your-trading-post-domain.com", 16 | "version": "0.2.0" 17 | }, 18 | "database": "./path/to/a/verto.db", 19 | "api": { 20 | "port": 8080, 21 | "host": "localhost" 22 | } 23 | } 24 | ``` 25 | 26 | - `blockedTokens`: This is the place for you to add all of the tokens you **don't** want your trading post to accept and exchange. By default, any PST is supported for trading. 27 | - `chain`: The object that contains the wallet addresses for accepting other currencies. Right now, Verto supports and requires trading with Ethereum, so your Ethereum address should go here. 28 | - `tradeFee`: This is the fee that your trading post will take when an exchange is made. You get to choose your own fee, but know that others may try to compete with lower fees! 29 | - `publicURL`: You'll need to add the publically available domain/IP that the trading post API will be accessible from in this variable. To ensure the uptime of a trading post, each trading post hosts its own API for the frontend to ping whenever a trade is initiated. 30 | - `version`: The current trading post version. 31 | - `database`: This field is where your database will be created. 32 | - `api`: If you want to modify the host and port of the API, you can do it in these variables. 33 | 34 | > You can easily change this configuration by updating `verto.config.json` and restarting your trading post! 35 | -------------------------------------------------------------------------------- /book/src/setup/installation.md: -------------------------------------------------------------------------------- 1 | ## Install pre-built binaries 2 | 3 | The trading post is distributed as standalone binaries for your operating system. 4 | Install these binaries via our installers - 5 | 6 | **Linux** 7 | 8 | ```shell script 9 | curl -fsSL https://verto.exchange/i/linux | sh 10 | ``` 11 | 12 | **MacOS** 13 | 14 | ```shell script 15 | curl -fsSL https://verto.exchange/i/mac | sh 16 | ``` 17 | 18 | **Windows** 19 | 20 | ```shell script 21 | iwr https://verto.exchange/i/windows | iex 22 | ``` 23 | 24 | It will download releases and unzip artifacts. You can add the binary to your PATH env variable by following the instructions after installation. 25 | -------------------------------------------------------------------------------- /book/src/setup/spin.md: -------------------------------------------------------------------------------- 1 | ## Start a trading post 2 | 3 | Starting your trading post is as simple as: 4 | 5 | ```shell script 6 | verto 7 | ``` 8 | 9 | If you want to start without having copying your keyfile to the project root, you can run with: 10 | 11 | ```shell script 12 | verto -k keyfile.json 13 | ``` 14 | 15 | Congratulations! You've successfully started a verto trading post! 🦔 16 | 17 | You can also [set up a reverse proxy](./docs/proxy.md) with `nginx` for hosting your API. 18 | -------------------------------------------------------------------------------- /e2e/Gemfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/useverto/trading-post/3f36f904c42c530686321ce49ed3ab88ce44e5be/e2e/Gemfile -------------------------------------------------------------------------------- /e2e/install.rb: -------------------------------------------------------------------------------- 1 | require_relative "logger" 2 | 3 | def install 4 | if OS.unix? 5 | exec_cmd('curl -fsSL http://localhost:3000/install/linux.sh | sh') 6 | else 7 | exec_cmd('pwsh -c "iwr http://localhost:3000/install/windows.ps1 | iex"') 8 | end 9 | end -------------------------------------------------------------------------------- /e2e/lib.js: -------------------------------------------------------------------------------- 1 | const Verto = require("@verto/lib"); 2 | 3 | let vrt = new Verto(null, null, { 4 | exchangeContract: "fE2OcfjlS-sHqG5K8QvxE8wHtcqKxS-YV0bDEgxo-eI", 5 | }); 6 | 7 | (async () => { 8 | console.log(await vrt.getTradingPosts()); 9 | })(); 10 | -------------------------------------------------------------------------------- /e2e/logger.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def black; "\e[30m#{self}\e[0m" end 3 | def red; "\e[31m#{self}\e[0m" end 4 | def green; "\e[32m#{self}\e[0m" end 5 | def brown; "\e[33m#{self}\e[0m" end 6 | def blue; "\e[34m#{self}\e[0m" end 7 | def magenta; "\e[35m#{self}\e[0m" end 8 | def cyan; "\e[36m#{self}\e[0m" end 9 | def gray; "\e[37m#{self}\e[0m" end 10 | 11 | def bg_black; "\e[40m#{self}\e[0m" end 12 | def bg_red; "\e[41m#{self}\e[0m" end 13 | def bg_green; "\e[42m#{self}\e[0m" end 14 | def bg_brown; "\e[43m#{self}\e[0m" end 15 | def bg_blue; "\e[44m#{self}\e[0m" end 16 | def bg_magenta; "\e[45m#{self}\e[0m" end 17 | def bg_cyan; "\e[46m#{self}\e[0m" end 18 | def bg_gray; "\e[47m#{self}\e[0m" end 19 | 20 | def bold; "\e[1m#{self}\e[22m" end 21 | def italic; "\e[3m#{self}\e[23m" end 22 | def underline; "\e[4m#{self}\e[24m" end 23 | def blink; "\e[5m#{self}\e[25m" end 24 | def reverse_color; "\e[7m#{self}\e[27m" end 25 | end 26 | 27 | def exec_cmd(str) 28 | puts "$ #{str.cyan.italic}" 29 | Open3.popen3(str) do |stdin, stdout, stderr, wait_thr| 30 | exit_status = wait_thr.value 31 | unless exit_status.success? 32 | abort "Fail".red 33 | end 34 | end 35 | end 36 | 37 | def exec_with_timeout(cmd, timeout) 38 | pid = Process.spawn(cmd, {[:err,:out] => :close, :pgroup => true}) 39 | begin 40 | Timeout.timeout(timeout) do 41 | Process.waitpid(pid, 0) 42 | $?.exitstatus == 0 43 | end 44 | rescue Timeout::Error 45 | Process.kill(15, -Process.getpgid(pid)) 46 | false 47 | end 48 | end 49 | 50 | def exec_bash(str) 51 | puts "$ bash -c #{str.cyan.italic}" 52 | Open3.popen3("bash", "-c", str) do |stdin, stdout, stderr, wait_thr| 53 | exit_status = wait_thr.value 54 | unless exit_status.success? 55 | abort "Fail".red 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /e2e/main.rb: -------------------------------------------------------------------------------- 1 | require_relative 'logger' 2 | require_relative 'os' 3 | require_relative 'install' 4 | 5 | require 'webrick' 6 | require 'open3' 7 | 8 | $default_contract = File.read('./src/utils/constants.yml') 9 | 10 | END { reset_contract() } 11 | 12 | def setup_contract 13 | File.write('./src/utils/constants.yml', ' 14 | exchangeContractSrc: fE2OcfjlS-sHqG5K8QvxE8wHtcqKxS-YV0bDEgxo-eI 15 | exchangeWallet: aLemOhg9OGovn-0o4cOCbueiHT9VgdYnpJpq7NgMA1A 16 | maxInt: 2147483647') 17 | end 18 | 19 | def reset_contract 20 | File.write('./src/utils/constants.yml', $default_contract) 21 | end 22 | 23 | def serve 24 | WEBrick::HTTPServer.new(:Port => 3000, :DocumentRoot => Dir.pwd, :Logger => WEBrick::Log.new(File::NULL), 25 | :AccessLog => []).start 26 | end 27 | 28 | def build 29 | exec_cmd("yarn pkg") 30 | end 31 | 32 | def setup_keyfile 33 | File.write('./arweave-keyfile.json', ENV['KEYFILE']) 34 | end 35 | 36 | def zip 37 | if OS.unix? 38 | exec_bash("sqlite3=$(find . -name node_sqlite3.node) 39 | cp $sqlite3 ./node_sqlite3.node 40 | zip -r verto.zip verto README.md LICENSE node_sqlite3.node 41 | rm ./node_sqlite3.node") 42 | else 43 | exec_cmd("pwsh -c \"$sqlite3=$(gci -filter \"node_sqlite3.node\" -af -s -name) 44 | Compress-Archive -CompressionLevel Optimal -Force -Path verto.exe, README.md, LICENSE, $sqlite3 -DestinationPath verto.zip\"") 45 | end 46 | end 47 | 48 | puts "VERTO INTEGRATION TEST SUITE 1.0".green.bold 49 | 50 | ENV['VERTO_URI'] = 'http://localhost:3000/verto.zip' 51 | 52 | Thread.new {serve()} 53 | setup_contract() 54 | build() 55 | zip() 56 | install() 57 | setup_keyfile() 58 | exec_cmd("./verto orders -c verto.config.example.json") 59 | Thread.new {exec_with_timeout("./verto -c verto.config.example.json -k arweave-keyfile.json", 60000)} 60 | exec_cmd("node ./e2e/lib.js") 61 | -------------------------------------------------------------------------------- /e2e/os.rb: -------------------------------------------------------------------------------- 1 | module OS 2 | def OS.windows? 3 | (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil 4 | end 5 | 6 | def OS.mac? 7 | (/darwin/ =~ RUBY_PLATFORM) != nil 8 | end 9 | 10 | def OS.unix? 11 | !OS.windows? 12 | end 13 | 14 | def OS.linux? 15 | OS.unix? and not OS.mac? 16 | end 17 | 18 | def OS.jruby? 19 | RUBY_ENGINE == 'jruby' 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /install/linux.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | if [ "$(uname -m)" != "x86_64" ]; then 4 | echo "Error: Unsupported architecture $(uname -m). Only x64 binaries are available." 1>&2 5 | exit 1 6 | fi 7 | 8 | if ! command -v unzip >/dev/null; then 9 | echo "Error: unzip is required to install Verto Trading Post." 1>&2 10 | exit 1 11 | fi 12 | 13 | if [ $# -eq 0 ]; then 14 | release_uri="https://github.com/useverto/trading-post/releases/latest/download/verto-x64-linux.zip" 15 | else 16 | release_uri="https://github.com/useverto/trading-post/releases/download/${1}/verto-x64-linux.zip" 17 | fi 18 | 19 | install_dir="${VERTO_INSTALL:-$HOME/.verto}" 20 | bin_dir="$install_dir" 21 | exe="$bin_dir/verto" 22 | 23 | if [ $VERTO_URI ]; then 24 | release_uri=$VERTO_URI 25 | fi 26 | 27 | if [ ! -d "$bin_dir" ]; then 28 | mkdir -p "$bin_dir" 29 | fi 30 | 31 | curl -#L -o "$exe.zip" "$release_uri" 32 | cd "$bin_dir" 33 | unzip -o "$exe.zip" 34 | chmod +x "$exe" 35 | rm "$exe.zip" 36 | 37 | echo "Verto Trading Post was installed successfully to $exe" 38 | if command -v verto >/dev/null; then 39 | echo "Run 'verto --help' to get started" 40 | else 41 | case $SHELL in 42 | /bin/zsh) shell_profile=".zshrc" ;; 43 | *) shell_profile=".bash_profile" ;; 44 | esac 45 | echo "Manually add the directory to your \$HOME/$shell_profile (or similar)" 46 | echo " export VERTO_INSTALL=\"$install_dir\"" 47 | echo " export PATH=\"\$VERTO_INSTALL:\$PATH\"" 48 | echo "Run '$exe --help' to get started" 49 | fi 50 | -------------------------------------------------------------------------------- /install/mac.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | if ! command -v unzip >/dev/null; then 4 | echo "Error: unzip is required to install Verto Trading Post." 1>&2 5 | exit 1 6 | fi 7 | 8 | if [ $# -eq 0 ]; then 9 | release_uri="https://github.com/useverto/trading-post/releases/latest/download/verto-x64-macos.zip" 10 | else 11 | release_uri="https://github.com/useverto/trading-post/releases/download/${1}/verto-x64-macos.zip" 12 | fi 13 | 14 | install_dir="${VERTO_INSTALL:-$HOME/.verto}" 15 | bin_dir="$install_dir" 16 | exe="$bin_dir/verto" 17 | 18 | if [ ! -d "$bin_dir" ]; then 19 | mkdir -p "$bin_dir" 20 | fi 21 | 22 | if [ $VERTO_URI ]; then 23 | release_uri=$VERTO_URI 24 | fi 25 | 26 | curl -#L -o "$exe.zip" "$release_uri" 27 | cd "$bin_dir" 28 | unzip -o "$exe.zip" 29 | chmod +x "$exe" 30 | rm "$exe.zip" 31 | 32 | echo "Verto Trading Post was installed successfully to $exe" 33 | if command -v verto >/dev/null; then 34 | echo "Run 'verto --help' to get started" 35 | else 36 | case $SHELL in 37 | /bin/zsh) shell_profile=".zshrc" ;; 38 | *) shell_profile=".bash_profile" ;; 39 | esac 40 | echo "Manually add the directory to your \$HOME/$shell_profile (or similar)" 41 | echo " export VERTO_INSTALL=\"$install_dir\"" 42 | echo " export PATH=\"\$VERTO_INSTALL:\$PATH\"" 43 | echo "Run '$exe --help' to get started" 44 | fi 45 | -------------------------------------------------------------------------------- /install/windows.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | if ($args.Length -eq 1) { 4 | $v = $args.Get(0) 5 | } 6 | 7 | $VertoInstall = $env:VERTO_INSTALL 8 | $BinDir = if ($VertoInstall) { 9 | "$VertoInstall" 10 | } else { 11 | "$Home\.verto" 12 | } 13 | 14 | $VertoZip = "$BinDir\verto.zip" 15 | $VertoExe = "$BinDir\verto.exe" 16 | 17 | # GitHub requires TLS 1.2 18 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 19 | 20 | $VertoUri = if (!$v) { 21 | "https://github.com/useverto/trading-post/releases/latest/download/verto-x64-windows.zip" 22 | } else { 23 | "https://github.com/useverto/trading-post/releases/download/${v}/verto-x64-windows.zip" 24 | } 25 | 26 | if ($env:VERTO_URI) { 27 | $VertoUri = $env:VERTO_URI 28 | } 29 | 30 | if (!(Test-Path $BinDir)) { 31 | New-Item $BinDir -ItemType Directory | Out-Null 32 | } 33 | 34 | Invoke-WebRequest $VertoUri -OutFile $VertoZip -UseBasicParsing 35 | 36 | if (Get-Command Expand-Archive -ErrorAction SilentlyContinue) { 37 | Expand-Archive $VertoZip -Destination $BinDir -Force 38 | } else { 39 | if (Test-Path $VertoExe) { 40 | Remove-Item $VertoExe 41 | } 42 | Add-Type -AssemblyName System.IO.Compression.FileSystem 43 | [IO.Compression.ZipFile]::ExtractToDirectory($VertoZip, $BinDir) 44 | } 45 | 46 | Remove-Item $VertoZip 47 | 48 | $User = [EnvironmentVariableTarget]::User 49 | $Path = [Environment]::GetEnvironmentVariable('Path', $User) 50 | if (!(";$Path;".ToLower() -like "*;$BinDir;*".ToLower())) { 51 | [Environment]::SetEnvironmentVariable('Path', "$Path;$BinDir", $User) 52 | $Env:Path += ";$BinDir" 53 | } 54 | 55 | Write-Output "Verto Trading Post was installed successfully to $VertoExe" 56 | Write-Output "Run 'verto --help' to get started" 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "verto-trading-post", 3 | "version": "2.0.0", 4 | "private": true, 5 | "author": "The Verto Team", 6 | "license": "MIT", 7 | "main": "./dist/verto.js", 8 | "bin": { 9 | "verto": "./dist/verto.js" 10 | }, 11 | "scripts": { 12 | "fmt": "prettier --write .", 13 | "fmt:check": "prettier --check .", 14 | "dev": "node . run -k arweave.json", 15 | "build": "rollup -c", 16 | "prod": "rollup -c --environment INCLUDE_DEPS,BUILD:production", 17 | "pkg": "yarn prod && pkg -t host dist/verto.js", 18 | "pkg:linux": "yarn prod && pkg -t node14-linux dist/verto.js", 19 | "pkg:macos": "yarn prod && pkg -t node14-macos dist/verto.js", 20 | "pkg:win": "yarn prod && pkg -t node14-win dist/verto.js", 21 | "pkg:arm": "echo not implemented", 22 | "pkg:all": "yarn prod && pkg dist/verto.js", 23 | "test": "rollup -c rollup.test.js && mocha 'build/verto.test.js'", 24 | "test:integration": "ruby ./e2e/main.rb" 25 | }, 26 | "dependencies": { 27 | "@koa/cors": "^3.1.0", 28 | "@koa/router": "^9.4.0", 29 | "@verto/lib": "^0.9.10", 30 | "arweave": "^1.10.13", 31 | "chalk": "^4.1.0", 32 | "commander": "^6.1.0", 33 | "dotenv": "^8.2.0", 34 | "enquirer": "^2.3.6", 35 | "koa": "^2.13.0", 36 | "node-fetch": "^2.6.1", 37 | "rfs": "^9.0.3", 38 | "rotating-file-stream": "^2.1.3", 39 | "smartweave": "^0.4.27", 40 | "sqlite": "^4.0.14", 41 | "sqlite3": "^5.0.0", 42 | "uuid": "^8.3.0", 43 | "web3": "^1.3.0" 44 | }, 45 | "devDependencies": { 46 | "@rollup/plugin-alias": "^3.1.1", 47 | "@rollup/plugin-commonjs": "^15.0.0", 48 | "@rollup/plugin-json": "^4.1.0", 49 | "@rollup/plugin-multi-entry": "^4.0.0", 50 | "@rollup/plugin-node-resolve": "^9.0.0", 51 | "@rollup/plugin-yaml": "^2.1.1", 52 | "@rollup/pluginutils": "^4.0.0", 53 | "@types/chai": "^4.2.12", 54 | "@types/chalk": "^2.2.0", 55 | "@types/commander": "^2.12.2", 56 | "@types/fecha": "^2.3.1", 57 | "@types/koa": "^2.11.4", 58 | "@types/koa__cors": "^3.0.1", 59 | "@types/koa__router": "^8.0.2", 60 | "@types/mocha": "^8.0.3", 61 | "@types/node": "^14.6.2", 62 | "@types/node-fetch": "^2.5.7", 63 | "@types/sinon": "^9.0.5", 64 | "@types/sqlite3": "^3.1.6", 65 | "@types/supertest": "^2.0.10", 66 | "@types/uuid": "^8.3.0", 67 | "@types/validator": "^13.1.0", 68 | "chai": "^4.2.0", 69 | "mocha": "^8.1.3", 70 | "module-alias": "^2.2.2", 71 | "pkg": "^4.4.9", 72 | "prettier": "^2.1.1", 73 | "rollup": "^2.26.6", 74 | "rollup-plugin-string": "^3.0.0", 75 | "rollup-plugin-terser": "^7.0.1", 76 | "rollup-plugin-typescript2": "^0.27.2", 77 | "sinon": "^9.0.3", 78 | "supertest": "^4.0.2", 79 | "ts-node": "^9.0.0", 80 | "typescript": "^4.0.2" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import typescript from "rollup-plugin-typescript2"; 3 | import pkg from "./package.json"; 4 | import { createFilter } from "@rollup/pluginutils"; 5 | import resolve from "@rollup/plugin-node-resolve"; 6 | import alias from "@rollup/plugin-alias"; 7 | import commonjs from "@rollup/plugin-commonjs"; 8 | import json from "@rollup/plugin-json"; 9 | import yml from "@rollup/plugin-yaml"; 10 | import { terser } from "rollup-plugin-terser"; 11 | 12 | const filter = createFilter("**/*.gql", []); 13 | 14 | const config = { 15 | input: "./src/index.ts", 16 | output: [ 17 | { 18 | file: pkg.main, 19 | format: "cjs", 20 | }, 21 | ], 22 | external: ["*.gql", "*.yml", "util", "fs", "path", "child_process", "assert"], 23 | plugins: [ 24 | typescript(), 25 | json(), 26 | yml(), 27 | commonjs({ 28 | include: ["node_modules/**"], 29 | ignoreGlobal: false, 30 | }), 31 | resolve({ 32 | preferBuiltins: true, 33 | jsnext: true, 34 | }), 35 | alias({ 36 | "@api": __dirname + "/src/api", 37 | "@endpoints": __dirname + "src/api/endpoints", 38 | "@utils": __dirname + "/src/utils", 39 | "@workflows": __dirname + "/src/workflows", 40 | "@commands": __dirname + "/src/commands", 41 | }), 42 | { 43 | name: "string", 44 | transform(code, id) { 45 | if (filter(id)) { 46 | return { 47 | code: `export default ${JSON.stringify(code)};`, 48 | map: { mappings: "" }, 49 | }; 50 | } 51 | }, 52 | }, 53 | terser({ 54 | format: { 55 | comments: false, 56 | }, 57 | }), 58 | ], 59 | }; 60 | 61 | if (!process.env.PROD) 62 | config.external.push(...Object.keys(pkg.dependencies || {})); 63 | 64 | export default config; 65 | -------------------------------------------------------------------------------- /rollup.test.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import cfg from "./rollup.config"; 3 | import multi from "@rollup/plugin-multi-entry"; 4 | 5 | cfg.output = [ 6 | { 7 | file: "./build/verto.test.js", 8 | format: "cjs", 9 | sourcemap: true, 10 | globals: { 11 | it: "it", 12 | describe: "describe", 13 | }, 14 | }, 15 | ]; 16 | cfg.plugins = [...cfg.plugins, multi()]; 17 | cfg.input = "test/**/*.test.ts"; 18 | 19 | export default cfg; 20 | -------------------------------------------------------------------------------- /scripts/fix-corrupt.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | const fetch = require("node-fetch"); 4 | 5 | async function request(graphql) { 6 | var requestOptions = { 7 | method: "POST", 8 | headers: { 9 | "content-type": "application/json", 10 | }, 11 | body: graphql, 12 | }; 13 | let res = await fetch("https://arweave.dev/graphql", requestOptions); 14 | return await res.clone().json(); 15 | } 16 | 17 | async function query({ query, variables }) { 18 | var graphql = JSON.stringify({ 19 | query, 20 | variables, 21 | }); 22 | return await request(graphql); 23 | } 24 | 25 | // 26 | 27 | const maxInt = 2147483647; 28 | 29 | // 30 | 31 | const Arweave = require("arweave"); 32 | 33 | async function fixCorrupt(jwk) { 34 | const client = Arweave.init({ 35 | host: "arweave.dev", 36 | port: 443, 37 | protocol: "https", 38 | }); 39 | 40 | const _txs = ( 41 | await query({ 42 | query: ` 43 | query ($tradingPost: String!) { 44 | transactions ( 45 | owners: [$tradingPost] 46 | first: ${maxInt} 47 | ) { 48 | edges { 49 | node { 50 | id 51 | quantity { 52 | ar 53 | } 54 | tags { 55 | name 56 | value 57 | } 58 | } 59 | } 60 | } 61 | } 62 | `, 63 | variables: { 64 | tradingPost: await client.wallets.jwkToAddress(JSON.parse(jwk)), 65 | }, 66 | }) 67 | ).data.transactions.edges; 68 | 69 | let txs = []; 70 | for (const tx of _txs) { 71 | if (parseFloat(tx.node.quantity.ar) > 0) { 72 | // AR transfer. 73 | } else { 74 | if ( 75 | tx.node.tags.find((tag) => tag.name === "Type") || 76 | tx.node.tags.find((tag) => tag.name === "Exchange") 77 | ) { 78 | // Confirmation tx or other exchange related tx 79 | } else { 80 | const resendTx = ( 81 | await query({ 82 | query: ` 83 | query($tradingPost: String!, $txID: [String!]!) { 84 | transactions( 85 | owners: [$tradingPost] 86 | tags: [ 87 | { name: "Exchange", values: "Verto" } 88 | { name: "Resend", values: $txID } 89 | ] 90 | ) { 91 | edges { 92 | node { 93 | id 94 | quantity { 95 | ar 96 | } 97 | tags { 98 | name 99 | value 100 | } 101 | } 102 | } 103 | } 104 | } 105 | `, 106 | variables: { 107 | tradingPost: await client.wallets.jwkToAddress(JSON.parse(jwk)), 108 | txID: tx.node.id, 109 | }, 110 | }) 111 | ).data.transactions.edges; 112 | 113 | if (resendTx.length === 0) { 114 | const tag = tx.node.tags.find((tag) => tag.name === "Input").value; 115 | const parsedTag = JSON.parse(tag); 116 | if (typeof parsedTag === "string") { 117 | txs.push({ 118 | id: tx.node.id, 119 | token: tx.node.tags.find((tag) => tag.name === "Contract").value, 120 | // If you parse the tag again, it will be correct 121 | input: JSON.parse(parsedTag), 122 | }); 123 | } else { 124 | // Not corrupt. 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | console.log(`Found ${txs.length} corrupt PST transactions.`); 132 | if (txs.length > 0) { 133 | console.log("\n***"); 134 | } 135 | 136 | for (const tx of txs) { 137 | console.log(`\nResending ${tx.id} ...`); 138 | const tags = { 139 | Exchange: "Verto", 140 | Resend: tx.id, 141 | "App-Name": "SmartWeaveAction", 142 | "App-Version": "0.3.0", 143 | Contract: tx.token, 144 | Input: JSON.stringify(tx.input), 145 | }; 146 | 147 | const resendTx = await client.createTransaction( 148 | { 149 | data: Math.random().toString().slice(-4), 150 | }, 151 | JSON.parse(jwk) 152 | ); 153 | for (const [key, value] of Object.entries(tags)) { 154 | resendTx.addTag(key, value); 155 | } 156 | 157 | await client.transactions.sign(resendTx, JSON.parse(jwk)); 158 | await client.transactions.post(resendTx); 159 | console.log(`Sent: ${resendTx.id}`); 160 | } 161 | } 162 | 163 | fixCorrupt(/* pass in your keyfile */); 164 | -------------------------------------------------------------------------------- /scripts/migrate.js: -------------------------------------------------------------------------------- 1 | const { open } = require("sqlite"); 2 | const sqlite3 = require("sqlite3"); 3 | 4 | async function collectTables(db) { 5 | let tables = await db.all( 6 | "SELECT name FROM sqlite_master WHERE type='table'" 7 | ); 8 | 9 | return tables 10 | .map((table) => { 11 | return table.name; 12 | }) 13 | .filter((table) => table !== "__verto__") 14 | .filter((table) => table !== "TX_STORE"); 15 | } 16 | 17 | async function main() { 18 | const sqlite = await open({ 19 | filename: "db.db", 20 | driver: sqlite3.Database, 21 | }); 22 | 23 | let tables = await collectTables(sqlite); 24 | 25 | tables.forEach(async (table) => { 26 | await sqlite.exec(`ALTER TABLE "${table}" ADD COLUMN token STRING;`); 27 | }); 28 | } 29 | 30 | main().catch(console.error); 31 | -------------------------------------------------------------------------------- /scripts/post_alpha_migration.js: -------------------------------------------------------------------------------- 1 | const { open } = require("sqlite"); 2 | const sqlite3 = require("sqlite3"); 3 | 4 | async function collectTables(db) { 5 | let tables = await db.all( 6 | "SELECT name FROM sqlite_master WHERE type='table'" 7 | ); 8 | 9 | return tables.map((table) => { 10 | return table.name; 11 | }); 12 | } 13 | 14 | async function main() { 15 | const sqlite = await open({ 16 | filename: "db.db", 17 | driver: sqlite3.Database, 18 | }); 19 | 20 | let tables = await collectTables(sqlite); 21 | tables = tables.filter((table) => table !== "__verto__"); 22 | 23 | tables.forEach(async (table) => { 24 | await sqlite.exec( 25 | `ALTER TABLE "${table}" ADD COLUMN received INTEGER NOT NULL DEFAULT 0;` 26 | ); 27 | }); 28 | } 29 | 30 | main().catch(console.error); 31 | -------------------------------------------------------------------------------- /scripts/refund.js: -------------------------------------------------------------------------------- 1 | const Arweave = require("arweave"); 2 | const fs = require("fs"); 3 | 4 | const client = new Arweave({ 5 | host: "arweave.net", 6 | port: 443, 7 | protocol: "https", 8 | }); 9 | const jwk = JSON.parse(fs.readFileSync("./arweave.json")); 10 | 11 | const orders = [ 12 | { 13 | id: "OkHNaEY3Z1WsASwT2gbgsudzZKKJadKRtjFjtcZEDMs", 14 | addr: "VovTpJyt97jf0WuE0eb8SujuQJ-IWi4OntFshIv9PV0", 15 | type: "Sell", 16 | amount: 541925, 17 | token: "usjm4PCxUd5mtaon7zc97-dt-3qf67yPyqgzLnLqk5A", 18 | }, 19 | { 20 | id: "v6CMUuuI9-IecXrVlpf2Qb7ZPMZgw1XmDyC5xkDWqOQ", 21 | addr: "XUAeWrINohr3c-x7Nm3x7n7hjbtzxFyjX5Bn0JZ_8a8", 22 | type: "Sell", 23 | amount: 250000, 24 | token: "usjm4PCxUd5mtaon7zc97-dt-3qf67yPyqgzLnLqk5A", 25 | }, 26 | { 27 | id: "gh_f8bjmxIOVqeHUlBFvBBa0u-pRwZxGFIfF2fPvZaA", 28 | addr: "XUAeWrINohr3c-x7Nm3x7n7hjbtzxFyjX5Bn0JZ_8a8", 29 | type: "Sell", 30 | amount: 250000, 31 | token: "usjm4PCxUd5mtaon7zc97-dt-3qf67yPyqgzLnLqk5A", 32 | }, 33 | { 34 | id: "0oog2TYJmo6K0mjpnG5aQy7dK8rXcM1yQdEm2rMtOkc", 35 | addr: "XUAeWrINohr3c-x7Nm3x7n7hjbtzxFyjX5Bn0JZ_8a8", 36 | type: "Sell", 37 | amount: 279598, 38 | token: "usjm4PCxUd5mtaon7zc97-dt-3qf67yPyqgzLnLqk5A", 39 | }, 40 | { 41 | id: "XpkBzrV3SnLor1UxXYccSXBVcoD_v-zV8_qrDYmRz3U", 42 | addr: "XUAeWrINohr3c-x7Nm3x7n7hjbtzxFyjX5Bn0JZ_8a8", 43 | type: "Sell", 44 | amount: 295222, 45 | token: "usjm4PCxUd5mtaon7zc97-dt-3qf67yPyqgzLnLqk5A", 46 | }, 47 | { 48 | id: "jgrNistb9Wvg-pm3xyhSA35ZskeRooiXGDqbb-siQbU", 49 | addr: "pvPWBZ8A5HLpGSEfhEmK1A3PfMgB_an8vVS6L14Hsls", 50 | type: "Swap", 51 | amount: 5, 52 | token: "", 53 | }, 54 | { 55 | id: "Z0xPm36ef1VTVilmIgtupKbeIvf3XxOAh9qdlqpevxc", 56 | addr: "vxUdiv2fGHMiIoek5E4l3M5qSuKCZtSaOBYjMRc94JU", 57 | type: "Buy", 58 | amount: 0.1, 59 | token: "", 60 | }, 61 | { 62 | id: "SglN92mbPASmx0dkeTQB8rm7iYh6_vOBzPHkWTFmQAI", 63 | addr: "pvPWBZ8A5HLpGSEfhEmK1A3PfMgB_an8vVS6L14Hsls", 64 | type: "Buy", 65 | amount: 0.05, 66 | token: "", 67 | }, 68 | { 69 | id: "6dV1P7WdZameiVOWpmZjOHCk_At1rUlambmeq87q79w", 70 | addr: "vxUdiv2fGHMiIoek5E4l3M5qSuKCZtSaOBYjMRc94JU", 71 | type: "Buy", 72 | amount: 0.1, 73 | token: "", 74 | }, 75 | { 76 | id: "And1f3z8bwopQKq75W6kO2D3aOAGs0LgYz96xQXy5Vs", 77 | addr: "vxUdiv2fGHMiIoek5E4l3M5qSuKCZtSaOBYjMRc94JU", 78 | type: "Buy", 79 | amount: 0.1, 80 | token: "", 81 | }, 82 | { 83 | id: "IbeuQ-enJOI8pfVPh39mfKMQr1DI3ttny1EvsaQj7IY", 84 | addr: "vxUdiv2fGHMiIoek5E4l3M5qSuKCZtSaOBYjMRc94JU", 85 | type: "Buy", 86 | amount: 0.1, 87 | token: "", 88 | }, 89 | { 90 | id: "kC9zbT9uq9u9A0vnI0bTxgsrBLHmk75YgpqTHRDsG7A", 91 | addr: "vxUdiv2fGHMiIoek5E4l3M5qSuKCZtSaOBYjMRc94JU", 92 | type: "Buy", 93 | amount: 0.5, 94 | token: "", 95 | }, 96 | { 97 | id: "y-yf9vS4MgkIOdG8H_4cUebRpW2jMtXWSH1vXc-x84Y", 98 | addr: "vxUdiv2fGHMiIoek5E4l3M5qSuKCZtSaOBYjMRc94JU", 99 | type: "Buy", 100 | amount: 0.2, 101 | token: "", 102 | }, 103 | { 104 | id: "70OE61kcZm6oMaDLmSvE3vOGNHmGpZ7Ltn6osuQaXXY", 105 | addr: "vxUdiv2fGHMiIoek5E4l3M5qSuKCZtSaOBYjMRc94JU", 106 | type: "Buy", 107 | amount: 0.2, 108 | token: "", 109 | }, 110 | { 111 | id: "XNkkc6Fbki6mG5eTpgonw9sPuOVG9AET94l6Sc-5QN0", 112 | addr: "vxUdiv2fGHMiIoek5E4l3M5qSuKCZtSaOBYjMRc94JU", 113 | type: "Buy", 114 | amount: 0.2, 115 | token: "", 116 | }, 117 | ]; 118 | 119 | async function refund() { 120 | for (const order of orders) { 121 | console.log("Order: ", order.id); 122 | if (order.token) { 123 | const tags = { 124 | Exchange: "Verto", 125 | Type: "Refund", 126 | Order: order.id, 127 | "App-Name": "SmartWeaveAction", 128 | "App-Version": "0.3.0", 129 | Contract: order.token, 130 | Input: JSON.stringify({ 131 | function: "transfer", 132 | target: order.addr, 133 | qty: order.amount, 134 | }), 135 | }; 136 | 137 | const tx = await client.createTransaction( 138 | { 139 | target: order.addr, 140 | data: Math.random().toString().slice(-4), 141 | }, 142 | jwk 143 | ); 144 | 145 | for (const [key, value] of Object.entries(tags)) { 146 | tx.addTag(key, value.toString()); 147 | } 148 | 149 | await client.transactions.sign(tx, jwk); 150 | await client.transactions.post(tx); 151 | 152 | console.log("Refund:", tx.id); 153 | } else { 154 | const tags = { 155 | Exchange: "Verto", 156 | Type: "Refund", 157 | Order: order.id, 158 | }; 159 | 160 | const tx = await client.createTransaction( 161 | { 162 | target: order.addr, 163 | quantity: client.ar.arToWinston(order.amount.toString()), 164 | }, 165 | jwk 166 | ); 167 | 168 | for (const [key, value] of Object.entries(tags)) { 169 | tx.addTag(key, value.toString()); 170 | } 171 | 172 | await client.transactions.sign(tx, jwk); 173 | await client.transactions.post(tx); 174 | console.log("Refund:", tx.id); 175 | } 176 | console.log(); 177 | } 178 | } 179 | 180 | refund(); 181 | -------------------------------------------------------------------------------- /scripts/refund_orders.js: -------------------------------------------------------------------------------- 1 | const { open } = require("sqlite"); 2 | const sqlite3 = require("sqlite3"); 3 | const fs = require("fs"); 4 | const Arweave = require("arweave"); 5 | 6 | // --- EDIT THESE --- // 7 | const DB_FILE = "./db.db"; 8 | const KEYFILE = "./arweave.json"; 9 | // ------------------ // 10 | 11 | const refundOrders = async (token) => { 12 | const db = await open({ 13 | filename: DB_FILE, 14 | driver: sqlite3.Database, 15 | }); 16 | const jwk = JSON.parse(await fs.readFileSync(KEYFILE)); 17 | const client = new Arweave({ 18 | host: "arweave.net", 19 | port: 443, 20 | protocol: "https", 21 | }); 22 | 23 | const orders = await db.all(`SELECT * FROM "${token}"`); 24 | 25 | for (const order of orders) { 26 | if (order.type === "Buy") { 27 | const tx = await client.createTransaction( 28 | { 29 | target: order.addr, 30 | quantity: client.ar.arToWinston(order.amnt.toString()), 31 | }, 32 | jwk 33 | ); 34 | await client.transactions.sign(tx, jwk); 35 | await client.transactions.post(tx); 36 | } 37 | 38 | if (order.type === "Sell") { 39 | const tags = { 40 | "App-Name": "SmartWeaveAction", 41 | "App-Version": "0.3.0", 42 | Contract: token, 43 | Input: JSON.stringify({ 44 | function: "transfer", 45 | target: order.addr, 46 | qty: order.amnt, 47 | }), 48 | }; 49 | 50 | const tx = await client.createTransaction( 51 | { 52 | target: order.addr, 53 | data: Math.random().toString().slice(-4), 54 | }, 55 | jwk 56 | ); 57 | 58 | for (const [key, value] of Object.entries(tags)) { 59 | tx.addTag(key, value.toString()); 60 | } 61 | 62 | await client.transactions.sign(tx, jwk); 63 | await client.transactions.post(tx); 64 | } 65 | 66 | await db.run(`DELETE FROM "${token}" WHERE txID = ?`, [order.txID]); 67 | } 68 | }; 69 | 70 | refundOrders(/* insert token id */); 71 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import Koa from "koa"; 2 | import Log from "@utils/logger"; 3 | import createRouter from "@api/routes"; 4 | import { v4 } from "uuid"; 5 | import { Database } from "sqlite"; 6 | import fetch from "node-fetch"; 7 | import cors from "@koa/cors"; 8 | 9 | const log = new Log({ 10 | level: Log.Levels.debug, 11 | name: "api", 12 | }); 13 | 14 | const http = new Koa(); 15 | 16 | http.use(cors()); 17 | 18 | // attach logger 19 | http.use(async (ctx, next) => { 20 | await next(); 21 | const rt = ctx.response.get("X-Response-Time"); 22 | log.debug(`${ctx.method} ${ctx.url} - ${rt}`); 23 | }); 24 | 25 | // set response time header - `x-response-time` 26 | http.use(async (ctx, next) => { 27 | const start = Date.now(); 28 | await next(); 29 | const ms = Date.now() - start; 30 | ctx.set("X-Response-Time", `${ms}ms`); 31 | }); 32 | 33 | /** 34 | * Start the trading post HTTP server 35 | */ 36 | export function initAPI( 37 | publicURL: string | URL, 38 | host?: string, 39 | port?: number, 40 | db?: Database, 41 | startItself: boolean = true 42 | ) { 43 | port = port || 8080; 44 | host = host || "localhost"; 45 | const verifyID = v4(); 46 | http.use(createRouter(db).routes()); 47 | if (startItself) http.listen(port, host); 48 | log.debug(`Started trading post server at port ${port}`); 49 | checkAvailability(publicURL); 50 | return http; 51 | } 52 | 53 | export function checkAvailability(url: string | URL) { 54 | let endpoint = String(url).endsWith("/") ? "ping" : "/ping"; 55 | fetch(`${url}/${endpoint}`).catch((err) => { 56 | log.warn("API is not publically accessible"); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/api/routes.ts: -------------------------------------------------------------------------------- 1 | import Router from "@koa/router"; 2 | import { Database } from "sqlite"; 3 | import { getOrders } from "@utils/database"; 4 | const router = new Router(); 5 | 6 | export default function createRouter(db?: Database): Router { 7 | router.all("/", async (ctx, next) => { 8 | ctx.state.db = db; 9 | await next(); 10 | }); 11 | /** 12 | * Endpoint for checking where a trading post is online. 13 | */ 14 | router.get("/ping", async (ctx, next) => { 15 | ctx.body = { 16 | uptime: process.uptime(), 17 | }; 18 | await next(); 19 | }); 20 | 21 | router.get("/orders", async (ctx, next) => { 22 | ctx.body = await getOrders(db!); 23 | await next(); 24 | }); 25 | 26 | return router; 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/init.ts: -------------------------------------------------------------------------------- 1 | import { prompt } from "enquirer"; 2 | import { createConfig, TradingPostConfig } from "@utils/config"; 3 | import verto from "../../package.json"; 4 | 5 | /** 6 | * Prompts user for input on fields for the configuration and writes to disk 7 | */ 8 | export default async () => { 9 | /** 10 | * Prompts for input and collects response 11 | */ 12 | const response: TradingPostConfig = await prompt([ 13 | { 14 | type: "list", 15 | name: "genesis.acceptedTokens", 16 | message: "Enter the contract ID for the supported token(s)", 17 | }, 18 | { 19 | type: "text", 20 | name: "genesis.tradeFee", 21 | message: "What will be the trade fee for the trading post?", 22 | }, 23 | { 24 | type: "input", 25 | name: "database", 26 | message: "Enter the database location", 27 | }, 28 | { 29 | type: "input", 30 | name: "api.host", 31 | message: "Enter trading post API host", 32 | }, 33 | { 34 | type: "input", 35 | name: "api.port", 36 | message: "Enter trading post API port", 37 | }, 38 | { 39 | type: "input", 40 | name: "genesis.publicURL", 41 | message: "Enter the publicly accessible url for the trading post", 42 | }, 43 | ]); 44 | 45 | response.genesis.version = verto.version; 46 | 47 | /** 48 | * Writes the configuration file to disk 49 | */ 50 | await createConfig("verto.config.json", response as TradingPostConfig); 51 | }; 52 | -------------------------------------------------------------------------------- /src/commands/orders.ts: -------------------------------------------------------------------------------- 1 | import { init, getOrders, OrderInstance } from "@utils/database"; 2 | import { asTree } from "@utils/tree"; 3 | import { loadConfig } from "@utils/config"; 4 | import chalk from "chalk"; 5 | 6 | /** 7 | * Display the trading post order book 8 | */ 9 | export default async (opts: any) => { 10 | let cnf = await loadConfig(opts.config); 11 | const connection = await init(cnf.database); 12 | let orders = await getOrders(connection); 13 | let orderTree: { 14 | [token: string]: OrderInstance[] | string | { [key: string]: any }; 15 | } = {}; 16 | orders.forEach((element) => { 17 | let token = chalk.italic.grey(element.token); 18 | orderTree[token] = 19 | element.orders.length == 0 ? "No orders" : element.orders; 20 | }); 21 | console.log(asTree(orderTree, true)); 22 | }; 23 | -------------------------------------------------------------------------------- /src/commands/upgrade.ts: -------------------------------------------------------------------------------- 1 | import { exec as execProcess } from "child_process"; 2 | import { promisify } from "util"; 3 | import Logger, { LogLevels } from "@utils/logger"; 4 | 5 | const exec = promisify(execProcess); 6 | 7 | const log = new Logger({ 8 | name: "upgrade", 9 | level: LogLevels.debug, 10 | }); 11 | 12 | type Installer = { 13 | [key in "linux" | "darwin" | "win32" | any]: string; 14 | }; 15 | 16 | const installers: Installer = { 17 | linux: `curl -fsSL https://verto.exchange/i/linux | sh`, 18 | /** darwin = macos */ 19 | darwin: `curl -fsSL https://verto.exchange/i/mac | sh`, 20 | /** win32 is the platform for both Windows 32bit and 64bit */ 21 | win32: `PowerShell -Command "& {Invoke-WebRequest https://verto.exchange/i/windows | Invoke-Expression}"`, 22 | }; 23 | 24 | export default async () => { 25 | if (process.platform in installers) { 26 | log.info(`Installing latest release build for ${process.platform}`); 27 | const { stdout, stderr } = await exec(installers[process.platform]); 28 | stderr && log.error(stderr); 29 | stdout && log.info(stdout); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.gql" { 2 | const content: string; 3 | export default content; 4 | } 5 | declare module "*.yml" { 6 | const content: { [x: string]: any }; 7 | export default content; 8 | } 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "@utils/console"; 2 | import dotenv from "dotenv"; 3 | import commander from "commander"; 4 | import InitCommand from "@commands/init"; 5 | import UpgradeCommand from "@commands/upgrade"; 6 | import OrdersCommand from "@commands/orders"; 7 | import Log from "@utils/logger"; 8 | import { initAPI } from "@api/index"; 9 | import { init as initDB, shutdownHook } from "@utils/database"; 10 | import { loadConfig } from "@utils/config"; 11 | import { bootstrap } from "@workflows/bootstrap"; 12 | 13 | /** 14 | * Initialize environment variables from .env 15 | */ 16 | dotenv.config(); 17 | 18 | /** 19 | * Create a logger instance 20 | */ 21 | const log = new Log({ 22 | level: Log.Levels.debug, 23 | name: "verto", 24 | }); 25 | 26 | /** 27 | * Create a CLI program and define argument flags 28 | */ 29 | const program = commander.program; 30 | program 31 | .name("verto") 32 | .version("2.0.0") 33 | .command("run", { isDefault: true }) 34 | /** 35 | * -k, --keyfile flag to specify the arweave keyfile location. 36 | */ 37 | .option("-k, --keyfile ", "Arweave wallet keyfile") 38 | /** 39 | * -eth-keyfile flag to specify ethereum private keyfile location 40 | */ 41 | .option("--eth-keyfile ", "Ethereum private keyfile") 42 | /** 43 | * -c, --config flag to specify verto's configuration file 44 | */ 45 | .option( 46 | "-c, --config ", 47 | "Verto trading post configuration", 48 | "verto.config.json" 49 | ) 50 | .action(RunCommand); 51 | 52 | /** 53 | * subcommand "init" to create a verto configuration file 54 | */ 55 | program 56 | .command("init") 57 | .description("generate a verto configuration file") 58 | .action(InitCommand); 59 | 60 | program 61 | .command("orders") 62 | /** 63 | * -c, --config flag to specify verto's configuration file 64 | */ 65 | .option( 66 | "-c, --config ", 67 | "Verto trading post configuration", 68 | "verto.config.json" 69 | ) 70 | .description("Show trading post order book") 71 | .action(OrdersCommand); 72 | 73 | /** 74 | * subcommand "upgrade" to upgrade to the latest trading post release 75 | */ 76 | program 77 | .command("upgrade") 78 | .description("upgrade to the latest trading post release") 79 | .action(UpgradeCommand); 80 | 81 | /** 82 | * Parse the raw process arguments 83 | */ 84 | program.parse(process.argv); 85 | 86 | /** 87 | * Starts the bootstrap process with the given keyfile and configuration 88 | */ 89 | async function RunCommand(opts: any) { 90 | /** 91 | * Load configuration from the provided config file 92 | */ 93 | loadConfig(opts.config).then(async (cnf) => { 94 | /** 95 | * Create a database connection pool and pass to all workflows 96 | */ 97 | let connPool = await initDB(cnf.database); 98 | /** 99 | * Instalise the trading post API 100 | */ 101 | initAPI(cnf.genesis.publicURL, cnf.api.host, cnf.api.port, connPool); 102 | /** 103 | * Start the bootstrap workflow 104 | */ 105 | await bootstrap(cnf, connPool, opts.keyfile, opts.ethKeyfile); 106 | /** 107 | * Setup shutdown hook 108 | */ 109 | shutdownHook(connPool); 110 | }); 111 | } 112 | -------------------------------------------------------------------------------- /src/queries/genesis.gql: -------------------------------------------------------------------------------- 1 | query($addr: String!, $exchange: String!) { 2 | transactions( 3 | owners: [$addr] 4 | recipients: [$exchange] 5 | tags: [ 6 | { name: "Exchange", values: "Verto" } 7 | { name: "Type", values: "Genesis" } 8 | ] 9 | first: 1 10 | ) { 11 | edges { 12 | node { 13 | id 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/queries/tx.gql: -------------------------------------------------------------------------------- 1 | query($txID: ID!) { 2 | transaction(id: $txID) { 3 | id 4 | owner { 5 | address 6 | } 7 | quantity { 8 | ar 9 | } 10 | tags { 11 | name 12 | value 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/queries/txs.gql: -------------------------------------------------------------------------------- 1 | query($recipients: [String!], $min: Int, $num: Int) { 2 | transactions( 3 | recipients: $recipients 4 | tags: [ 5 | { name: "Exchange", values: "Verto" } 6 | { name: "Type", values: ["Buy", "Sell", "Cancel", "Swap"] } 7 | ] 8 | block: { min: $min } 9 | first: $num 10 | ) { 11 | edges { 12 | node { 13 | id 14 | block { 15 | height 16 | timestamp 17 | } 18 | owner { 19 | address 20 | } 21 | quantity { 22 | ar 23 | } 24 | tags { 25 | name 26 | value 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/arweave.ts: -------------------------------------------------------------------------------- 1 | import Arweave from "arweave"; 2 | import { JWKPublicInterface } from "arweave/node/lib/wallet"; 3 | import Logger from "@utils/logger"; 4 | import { relative } from "path"; 5 | import * as fs from "fs"; 6 | import { Database } from "sqlite"; 7 | import { getTimestamp, getTxStore, saveHash } from "@utils/database"; 8 | import { query } from "@utils/gql"; 9 | import txsQuery from "../queries/txs.gql"; 10 | import CONSTANTS from "../utils/constants.yml"; 11 | import { readContract } from "smartweave"; 12 | 13 | const { readFile } = fs.promises; 14 | 15 | const relativeKeyPath = process.env.KEY_PATH 16 | ? relative(__dirname, process.env.KEY_PATH) 17 | : "./arweave.json"; 18 | 19 | const log = new Logger({ 20 | level: Logger.Levels.debug, 21 | name: "arweave", 22 | }); 23 | 24 | export async function init(keyfile?: string) { 25 | const client = new Arweave({ 26 | host: "amp-gw.online", 27 | port: 443, 28 | protocol: "https", 29 | timeout: 20000, 30 | logging: false, 31 | logger: (msg: any) => { 32 | if (new Error().stack?.includes("smartweave")) return; 33 | log.debug(msg); 34 | }, 35 | }); 36 | 37 | const jwk = await getJwk(keyfile); 38 | const addr = await client.wallets.jwkToAddress(jwk!); 39 | const balance = client.ar.winstonToAr(await client.wallets.getBalance(addr)); 40 | 41 | log.info( 42 | "Created Arweave instance:\n\t\t" + 43 | `addr = ${addr}\n\t\t` + 44 | `balance = ${parseFloat(balance).toFixed(3)} AR` 45 | ); 46 | 47 | return { client, addr, jwk }; 48 | } 49 | 50 | let cachedJwk: JWKPublicInterface | undefined; 51 | export async function getJwk(keyfile?: string) { 52 | if (!cachedJwk) { 53 | log.info(`Loading keyfile from: ${keyfile || relativeKeyPath}`); 54 | const potentialJwk = JSON.parse( 55 | await readFile(keyfile || relativeKeyPath, { encoding: "utf8" }) 56 | ); 57 | cachedJwk = potentialJwk; 58 | } 59 | return cachedJwk; 60 | } 61 | 62 | export const latestTxs = async ( 63 | client: Arweave, 64 | db: Database, 65 | addr: string, 66 | latest: { 67 | block: number; 68 | txID: string; 69 | } 70 | ): Promise<{ 71 | txs: { 72 | id: string; 73 | block: number; 74 | sender: { ar: string; eth?: string }; 75 | type: string; 76 | table?: string; 77 | token?: string; 78 | order?: string; 79 | arAmnt?: number; 80 | amnt?: number; 81 | rate?: number; 82 | }[]; 83 | latest: { 84 | block: number; 85 | txID: string; 86 | }; 87 | }> => { 88 | const timeEntries = await getTimestamp(db); 89 | const time = timeEntries[timeEntries.length - 1]["createdAt"] 90 | .toString() 91 | .slice(0, -3); 92 | 93 | let _txs = ( 94 | await query({ 95 | query: txsQuery, 96 | variables: { 97 | recipients: [addr], 98 | min: latest.block, 99 | num: CONSTANTS.maxInt, 100 | }, 101 | }) 102 | ).data.transactions.edges.reverse(); 103 | 104 | let index: number = 0; 105 | for (let i = 0; i < _txs.length; i++) { 106 | if (_txs[i].node.id === latest.txID) { 107 | index = i + 1; 108 | break; 109 | } 110 | } 111 | _txs = _txs.slice(index, _txs.length); 112 | 113 | const txs: { 114 | id: string; 115 | block: number; 116 | sender: { ar: string; eth?: string }; 117 | type: string; 118 | table?: string; 119 | token?: string; 120 | order?: string; 121 | arAmnt?: number; 122 | amnt?: number; 123 | rate?: number; 124 | }[] = []; 125 | 126 | for (const tx of _txs) { 127 | if (tx.node.block.timestamp > time) { 128 | const type = tx.node.tags.find( 129 | (tag: { name: string; value: string }) => tag.name === "Type" 130 | ).value; 131 | 132 | if (type === "Buy") { 133 | txs.push({ 134 | id: tx.node.id, 135 | block: tx.node.block.height, 136 | sender: { ar: tx.node.owner.address }, 137 | type, 138 | table: tx.node.tags.find( 139 | (tag: { name: string; value: string }) => tag.name === "Token" 140 | ).value, 141 | arAmnt: parseFloat(tx.node.quantity.ar), 142 | }); 143 | } else if (type === "Sell") { 144 | const contract = tx.node.tags.find( 145 | (tag: { name: string; value: string }) => tag.name === "Contract" 146 | ).value; 147 | const res = await readContract(client, contract, undefined, true); 148 | 149 | if (res.validity[tx.node.id]) { 150 | const input = JSON.parse( 151 | tx.node.tags.find( 152 | (tag: { name: string; value: string }) => tag.name === "Input" 153 | ).value 154 | ); 155 | 156 | if (input.function === "transfer" && input.target === addr) { 157 | txs.push({ 158 | id: tx.node.id, 159 | block: tx.node.block.height, 160 | sender: { ar: tx.node.owner.address }, 161 | type, 162 | table: contract, 163 | amnt: input.qty, 164 | rate: tx.node.tags.find( 165 | (tag: { name: string; value: string }) => tag.name === "Rate" 166 | ).value, 167 | }); 168 | } 169 | } 170 | } else if (type === "Cancel") { 171 | txs.push({ 172 | id: tx.node.id, 173 | block: tx.node.block.height, 174 | sender: { ar: tx.node.owner.address }, 175 | type, 176 | order: tx.node.tags.find( 177 | (tag: { name: string; value: string }) => tag.name === "Order" 178 | ).value, 179 | }); 180 | } else if (type === "Swap") { 181 | const hashTag = tx.node.tags.find( 182 | (tag: { name: string; value: string }) => tag.name === "Hash" 183 | ); 184 | if (hashTag) { 185 | let store: { 186 | txHash: string; 187 | chain: string; 188 | token?: string; 189 | sender: string; 190 | }[] = []; 191 | try { 192 | store = await db.all(`SELECT * FROM "TX_STORE"`); 193 | } catch { 194 | // do nothing 195 | } 196 | 197 | if (store.find((element) => element.txHash === hashTag.value)) { 198 | // don't do anything, already parsed 199 | } else { 200 | await saveHash(db, { 201 | txHash: hashTag.value, 202 | chain: tx.node.tags.find( 203 | (tag: { name: string; value: string }) => tag.name === "Chain" 204 | ).value, 205 | token: tx.node.tags.find( 206 | (tag: { name: string; value: string }) => tag.name === "Token" 207 | )?.value, 208 | sender: tx.node.owner.address, 209 | }); 210 | } 211 | } else { 212 | const table = tx.node.tags.find( 213 | (tag: { name: string; value: string }) => tag.name === "Chain" 214 | ).value; 215 | txs.push({ 216 | id: tx.node.id, 217 | block: tx.node.block.height, 218 | sender: { 219 | ar: tx.node.owner.address, 220 | eth: await getChainAddr(tx.node.owner.address, table), 221 | }, 222 | type, 223 | table, 224 | arAmnt: parseFloat(tx.node.quantity.ar), 225 | rate: tx.node.tags.find( 226 | (tag: { name: string; value: string }) => tag.name === "Rate" 227 | ).value, 228 | }); 229 | } 230 | } 231 | } 232 | } 233 | 234 | let newLatest = latest; 235 | if (txs.length > 0) { 236 | newLatest = { 237 | block: txs[txs.length - 1].block, 238 | txID: txs[txs.length - 1].id, 239 | }; 240 | } 241 | 242 | return { txs, latest: newLatest }; 243 | }; 244 | 245 | export const getArAddr = async ( 246 | addr: string, 247 | chain: string 248 | ): Promise => { 249 | let txs = ( 250 | await query({ 251 | query: ` 252 | query($addr: [String!]!, $chain: [String!]!) { 253 | transactions( 254 | tags: [ 255 | { name: "Application", values: "ArLink" } 256 | { name: "Chain", values: $chain } 257 | { name: "Wallet", values: $addr } 258 | ] 259 | first: 1 260 | ) { 261 | edges { 262 | node { 263 | owner { 264 | address 265 | } 266 | } 267 | } 268 | } 269 | } 270 | `, 271 | variables: { 272 | addr, 273 | chain, 274 | }, 275 | }) 276 | ).data.transactions.edges; 277 | 278 | if (txs.length === 1) { 279 | return txs[0].node.owner.address; 280 | } 281 | }; 282 | 283 | export const getChainAddr = async ( 284 | addr: string, 285 | chain: string 286 | ): Promise => { 287 | let txs = ( 288 | await query({ 289 | query: ` 290 | query($addr: String!, $chain: [String!]!) { 291 | transactions( 292 | owners: [$addr] 293 | tags: [ 294 | { name: "Application", values: "ArLink" } 295 | { name: "Chain", values: $chain } 296 | ] 297 | first: 1 298 | ) { 299 | edges { 300 | node { 301 | tags { 302 | name 303 | value 304 | } 305 | } 306 | } 307 | } 308 | } 309 | `, 310 | variables: { 311 | addr, 312 | chain, 313 | }, 314 | }) 315 | ).data.transactions.edges; 316 | 317 | if (txs.length === 1) { 318 | const tag = txs[0].node.tags.find( 319 | (tag: { name: string; value: string }) => tag.name === "Wallet" 320 | ); 321 | 322 | if (tag) { 323 | return tag.value; 324 | } 325 | } 326 | }; 327 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { URL } from "url"; 3 | import Logger from "@utils/logger"; 4 | 5 | const log = new Logger({ 6 | name: "config", 7 | level: Logger.Levels.debug, 8 | }); 9 | 10 | const { readFile, writeFile } = fs.promises; 11 | 12 | /** 13 | * Trading post API Server configuration 14 | */ 15 | export interface APIConfig { 16 | host: string; 17 | port: number; 18 | } 19 | 20 | /** 21 | * Trading post Genesis info 22 | */ 23 | export interface GenesisConfig { 24 | blockedTokens: string[]; 25 | chain: Chain; 26 | tradeFee: number; 27 | version: string; 28 | publicURL: URL | string; 29 | } 30 | 31 | export interface Chain { 32 | [chain: string]: { 33 | addr: string; 34 | node?: string; 35 | }; 36 | } 37 | 38 | /** 39 | * The Trading post configuration 40 | */ 41 | export interface TradingPostConfig { 42 | genesis: GenesisConfig; 43 | database: string; 44 | api: APIConfig; 45 | } 46 | 47 | /** 48 | * Basically a gtfo-if-not-valid utility for validating config 49 | * @param msg message to log while validating the verto configuration 50 | */ 51 | const logValidate = (msg: string) => { 52 | log.error(`Config validation failed: ${msg}`); 53 | process.exit(1); 54 | }; 55 | 56 | /** 57 | * The trading post config that needs to be validated at runtime. 58 | * @param obj A don't-know-if-valid trading post configuration 59 | */ 60 | export function validateConfig(obj: TradingPostConfig) { 61 | obj.genesis.blockedTokens && 62 | !obj.genesis.blockedTokens.every((i) => typeof i === "string") && 63 | logValidate("tokens must be string and nothing else"); 64 | obj.genesis.tradeFee && 65 | typeof obj.genesis.tradeFee !== "number" && 66 | logValidate("trade free must be of a valid integer"); 67 | typeof obj.genesis.tradeFee !== "number" && 68 | logValidate("database location must be a string"); 69 | typeof obj.genesis.version !== "string" && 70 | logValidate("version must be a string"); 71 | } 72 | 73 | /** 74 | * Read the trading post config 75 | * @param loc Location of the config file 76 | */ 77 | export async function loadConfig(loc: string): Promise { 78 | try { 79 | let config: TradingPostConfig = JSON.parse( 80 | await readFile(loc, { encoding: "utf8" }) 81 | ); 82 | validateConfig(config); 83 | log.info(`Loaded config file from ${loc}`); 84 | return config; 85 | } catch (e) { 86 | log.error(`Failed to deserialize trading post config: ${e}`); 87 | process.exit(1); 88 | } 89 | } 90 | 91 | /** 92 | * Write the trading post configuration to a json file 93 | * @param loc Location of the desired config file 94 | * @param config The trading post configuration 95 | */ 96 | export async function createConfig(loc: string, config: TradingPostConfig) { 97 | try { 98 | await writeFile(loc, JSON.stringify(config, null, 2), { encoding: "utf8" }); 99 | log.info(`Created Verto configuration file at ${loc}`); 100 | } catch (e) { 101 | log.error(`Failed to write trading post config: ${e}`); 102 | process.exit(1); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/utils/console.ts: -------------------------------------------------------------------------------- 1 | import Logger from "@utils/logger"; 2 | const log = new Logger({ 3 | name: "verto", 4 | level: Logger.Levels.debug, 5 | }); 6 | 7 | /** 8 | * Converts console to logger except the console.log method 9 | */ 10 | 11 | console.info = (x: any) => log.info(x); 12 | console.warn = (x: any) => log.warn(x); 13 | console.error = (x: any) => log.error(x); 14 | -------------------------------------------------------------------------------- /src/utils/constants.yml: -------------------------------------------------------------------------------- 1 | exchangeContractSrc: usjm4PCxUd5mtaon7zc97-dt-3qf67yPyqgzLnLqk5A 2 | exchangeWallet: aLemOhg9OGovn-0o4cOCbueiHT9VgdYnpJpq7NgMA1A 3 | 4 | # integers on the server are 32 bit so we can't user Number.MAX_SAFE_INTEGER 5 | maxInt: 2147483647 6 | -------------------------------------------------------------------------------- /src/utils/database.ts: -------------------------------------------------------------------------------- 1 | import sqlite3 from "sqlite3"; 2 | import { open, Database } from "sqlite"; 3 | import Logger from "@utils/logger"; 4 | 5 | const log = new Logger({ 6 | name: "database", 7 | level: Logger.Levels.debug, 8 | }); 9 | 10 | type Order = "Buy" | "Sell"; 11 | 12 | // We need to declare an interface for our model that is basically what our class would be 13 | export interface OrderInstance { 14 | txID: string; 15 | amnt: number; 16 | rate?: number; 17 | addr: string; 18 | type: Order; 19 | createdAt: Date; 20 | received: number; 21 | token?: string; 22 | } 23 | 24 | /** 25 | * Establish connection with the sqlite database. 26 | * @param db sqlite data file location 27 | */ 28 | export async function init(db: string): Promise { 29 | const sqlite = await open({ 30 | filename: db, 31 | driver: sqlite3.Database, 32 | }); 33 | 34 | await sqlite.exec(` 35 | PRAGMA journal_mode=WAL; 36 | PRAGMA temp_store=memory; 37 | PRAGMA page_size=4096; 38 | PRAGMA mmap_size=6000000; 39 | PRAGMA optimize; 40 | `); 41 | 42 | return sqlite; 43 | } 44 | 45 | /** 46 | * Save a buy or sell order in the database 47 | * @param db sqlite3 connection pool 48 | * @param table 49 | * @param entry the order instance 50 | */ 51 | export async function saveOrder( 52 | db: Database, 53 | table: string, 54 | entry: OrderInstance 55 | ) { 56 | await db.exec(`CREATE TABLE IF NOT EXISTS "${table}" ( 57 | txID STRING NOT NULL PRIMARY KEY, 58 | amnt INTEGER NOT NULL, 59 | rate INTEGER, 60 | addr STRING NOT NULL, 61 | type STRING NOT NULL, 62 | createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 63 | received INTEGER NOT NULL, 64 | token STRING 65 | )`); 66 | /** 67 | * Insert a token instance into the database. 68 | * NOTE: The following code is not vulnerable to sql injection since invalid table names can never be queried. 69 | * The values are assigned via db.run that is capable of preventing any type of injection 70 | */ 71 | return await db.run( 72 | `INSERT INTO "${table}" VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 73 | [ 74 | entry.txID, 75 | entry.amnt, 76 | entry.rate, 77 | entry.addr, 78 | entry.type, 79 | entry.createdAt, 80 | entry.received, 81 | entry.token, 82 | ] 83 | ); 84 | } 85 | 86 | export async function getOrders(db: Database) { 87 | let tables: { name: string }[] = await db.all( 88 | "SELECT name FROM sqlite_master WHERE type='table'" 89 | ); 90 | 91 | let orders: { token: string; orders: OrderInstance[] }[] = []; 92 | 93 | for (const table of tables) { 94 | if (table.name !== "__verto__") { 95 | orders.push({ 96 | token: table.name, 97 | orders: await db.all(`SELECT * FROM "${table.name}"`), 98 | }); 99 | } 100 | } 101 | 102 | return orders; 103 | } 104 | 105 | /** 106 | * Retreive sell orders from the database and sort them by their price. 107 | * @param db sqlite3 connection pool 108 | * @param table 109 | */ 110 | export async function getSellOrders( 111 | db: Database, 112 | table: string 113 | ): Promise { 114 | /** 115 | * Retrieve sell orders from the database. 116 | * NOTE: The following code is not vulnerable to sql injection as it is merely retreiving data. 117 | */ 118 | let orders: OrderInstance[]; 119 | try { 120 | orders = await db.all( 121 | `SELECT * FROM "${table}" WHERE type = "Sell"` 122 | ); 123 | } catch { 124 | // table doesn't exist 125 | orders = []; 126 | } 127 | if (!orders || orders?.length === 0) { 128 | log.info(`No sell orders to match with.`); 129 | return []; 130 | } 131 | /** 132 | * Sort orders by their rate 133 | */ 134 | orders.sort((a, b) => { 135 | if (a.rate && b.rate) return b.rate - a.rate; 136 | else return 0; 137 | }); 138 | return orders; 139 | } 140 | 141 | /** 142 | * Retreive buy orders from the database and sort them by date of creation. 143 | * @param db sqlite3 connection pool 144 | * @param table 145 | */ 146 | export async function getBuyOrders( 147 | db: Database, 148 | table: string 149 | ): Promise { 150 | /** 151 | * Retrieve sell orders from the database. 152 | * NOTE: The following code is not vulnerable to sql injection as it is merely retreiving data. 153 | */ 154 | let orders: OrderInstance[]; 155 | try { 156 | orders = await db.all( 157 | `SELECT * FROM "${table}" WHERE type = "Buy"` 158 | ); 159 | } catch { 160 | // table doesn't exist 161 | orders = []; 162 | } 163 | if (!orders || orders?.length === 0) { 164 | log.info(`No buy orders to match with.`); 165 | return []; 166 | } 167 | /** 168 | * Sort orders by their rate 169 | */ 170 | orders.sort((a, b) => { 171 | if (a.rate && b.rate) return a.rate - b.rate; 172 | else return 0; 173 | }); 174 | return orders; 175 | } 176 | 177 | export async function getOrder( 178 | db: Database, 179 | table: string, 180 | txID: string 181 | ): Promise { 182 | const order = await db.get( 183 | `SELECT * FROM "${table}" WHERE txID = "${txID}"` 184 | ); 185 | return order!; 186 | } 187 | 188 | /** 189 | * Save last alive timestamp in database 190 | * @param db the database connection pool 191 | */ 192 | export async function saveTimestamp(db: Database) { 193 | await db.exec(`CREATE TABLE IF NOT EXISTS "__verto__" ( 194 | createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP 195 | )`); 196 | log.info(`Performing shutdown cleanup.`); 197 | await db.run(`INSERT INTO "__verto__" VALUES (?)`, [new Date()]); 198 | } 199 | 200 | interface DbTimestamp { 201 | createdAt: Date | string; 202 | } 203 | 204 | /** 205 | * Get the timestamp from database 206 | * @param db the database connection pool 207 | */ 208 | export async function getTimestamp(db: Database): Promise { 209 | try { 210 | return await db.all(`SELECT * FROM "__verto__"`); 211 | } catch { 212 | await db.exec(`CREATE TABLE "__verto__" ( 213 | createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP 214 | )`); 215 | await db.run(`INSERT INTO "__verto__" VALUES (?)`, [new Date()]); 216 | return await getTimestamp(db); 217 | } 218 | } 219 | 220 | /** 221 | * Setup post shutdown hook and store last uptime in database 222 | * @param db the database connection pool 223 | */ 224 | export async function shutdownHook(db: Database): Promise { 225 | // attach user callback to the process event emitter 226 | // if no callback, it will still exit gracefully on Ctrl-C 227 | process.on("cleanup", () => saveTimestamp(db)); 228 | 229 | // do app specific cleaning before exiting 230 | process.on("exit", async function () { 231 | await saveTimestamp(db); 232 | }); 233 | 234 | // catch ctrl+c event and exit normally 235 | process.on("SIGINT", async function () { 236 | await saveTimestamp(db); 237 | process.exit(2); 238 | }); 239 | 240 | //catch uncaught exceptions, trace, then exit normally 241 | process.on("uncaughtException", async function () { 242 | await saveTimestamp(db); 243 | process.exit(99); 244 | }); 245 | } 246 | 247 | export async function saveHash( 248 | db: Database, 249 | entry: { 250 | txHash: string; 251 | chain: string; 252 | token?: string; 253 | sender: string; 254 | } 255 | ) { 256 | await db.exec(`CREATE TABLE IF NOT EXISTS "TX_STORE" ( 257 | txHash STRING NOT NULL PRIMARY KEY, 258 | parsed INTEGER NOT NULL DEFAULT 0, 259 | chain STRING NOT NULL, 260 | token STRING, 261 | sender STRING NOT NULL 262 | )`); 263 | 264 | return await db.run(`INSERT INTO "TX_STORE" VALUES (?, ?, ?, ?, ?)`, [ 265 | entry.txHash, 266 | 0, 267 | entry.chain, 268 | entry.token, 269 | entry.sender, 270 | ]); 271 | } 272 | 273 | export async function getTxStore( 274 | db: Database 275 | ): Promise< 276 | { 277 | txHash: string; 278 | chain: string; 279 | token?: string; 280 | sender: string; 281 | }[] 282 | > { 283 | let store: { 284 | txHash: string; 285 | chain: string; 286 | token?: string; 287 | sender: string; 288 | }[] = []; 289 | try { 290 | store = await db.all(`SELECT * FROM "TX_STORE" WHERE parsed = 0`); 291 | } catch { 292 | // do nothing 293 | } 294 | 295 | return store; 296 | } 297 | -------------------------------------------------------------------------------- /src/utils/eth.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { relative } from "path"; 3 | import Logger from "@utils/logger"; 4 | import Web3 from "web3"; 5 | 6 | const { readFile } = fs.promises; 7 | 8 | const relativeKeyPath = process.env.KEY_PATH 9 | ? relative(__dirname, process.env.KEY_PATH) 10 | : "./privatekey"; 11 | 12 | const log = new Logger({ 13 | level: Logger.Levels.debug, 14 | name: "eth", 15 | }); 16 | 17 | export async function init(keyfile?: string, url?: string) { 18 | log.info(`Loading private key from: ${keyfile || relativeKeyPath}`); 19 | const privateKey = (await readFile(keyfile || relativeKeyPath)) 20 | .toString() 21 | .split("\n")[0]; 22 | 23 | const client = new Web3( 24 | url || 25 | "https://eth-mainnet.alchemyapi.io/v2/U5zYjOBafrwXy-2jZY6HMa0lOrsFge9K" 26 | ); 27 | 28 | const account = client.eth.accounts.privateKeyToAccount(privateKey); 29 | const balance = client.utils.fromWei( 30 | await client.eth.getBalance(account.address), 31 | "ether" 32 | ); 33 | 34 | log.info( 35 | "Created Web3 instance:\n\t\t" + 36 | `addr = ${account.address}\n\t\t` + 37 | `balance = ${parseFloat(balance).toFixed(3)} ETH` 38 | ); 39 | 40 | return { client, addr: account.address, sign: account.signTransaction }; 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/gql.ts: -------------------------------------------------------------------------------- 1 | // Client for the Arweave GraphQL endpoint 2 | import fetch from "node-fetch"; 3 | 4 | interface StringMap { 5 | [key: string]: string | object | number; 6 | } 7 | 8 | /** 9 | * Represents a graphql query 10 | */ 11 | interface GrapqlQuery { 12 | /** 13 | * The graphql query as a string 14 | */ 15 | query: string; 16 | /** 17 | * The graphql variables in the given query. 18 | */ 19 | variables?: string | StringMap; 20 | } 21 | 22 | /** 23 | * Perform a HTTP request to the graphql server. 24 | * @param graphql The response body as string 25 | */ 26 | async function request(graphql: string) { 27 | var requestOptions = { 28 | method: "POST", 29 | headers: { 30 | "content-type": "application/json", 31 | }, 32 | body: graphql, 33 | }; 34 | let res = await fetch("https://amp-gw.online/graphql", requestOptions); 35 | return await res.clone().json(); 36 | } 37 | 38 | /** 39 | * Execute a graphql query with variables. 40 | * @param param0 A graphql query and its vaiables. 41 | */ 42 | export async function query({ query, variables }: GrapqlQuery) { 43 | var graphql = JSON.stringify({ 44 | query, 45 | variables, 46 | }); 47 | return await request(graphql); 48 | } 49 | 50 | /** 51 | * Execute a simple graphql query without variables. 52 | * @param query The graphql query to be executed. 53 | */ 54 | export async function simpleQuery(query: string) { 55 | var graphql = JSON.stringify({ 56 | query, 57 | variables: {}, 58 | }); 59 | return await request(graphql); 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from "stream"; 2 | import chalk from "chalk"; 3 | import fecha from "fecha"; 4 | // @ts-ignore 5 | import * as rfs from "rotating-file-stream"; 6 | 7 | /** 8 | * Levels enum used to determine when to log, `none` means nothing will be logged. 9 | */ 10 | export enum LogLevels { 11 | "none" = -1, 12 | "error", 13 | "warn", 14 | "info", 15 | "debug", 16 | } 17 | 18 | /** 19 | * Options for the Log class. Options marked as `undefined` are optional. 20 | * @param {boolean | undefined} console `boolean | undefined` Whether to log to console or not. Defaults to `process.env.NODE_ENV === 'development'`. 21 | * @param {LogLevels | undefined} level `LogLevels | undefined` Determines what levels it should log. Default to `LogLevels.debug` if `process.env.NODE_ENV === 'development'`, otherwise `LogLevels.info`. 22 | * @param {string} name `string` Determines the filename from `logs/${name}.log` to output as. 23 | * @param {Writable | undefined} stream `Writable | undefined` The stream Log should write to. Defaults to a gzipped [`rotating-file-stream`](https://www.npmjs.com/package/rotating-file-stream) that rotates every 5 days with a maximum of 6 files. 24 | * @param {boolean | string | undefined} timestamp `boolean | string | undefined` If `true` or `undefined`, defaults to `YYYY-MM-DD HH:mm:ss,SSS` with [Fecha's formatting tokens](https://www.npmjs.com/package/fecha#formatting-tokens). If `false` or an empty string, doesn't output a timestamp at all. If a non-empty string, uses that as formatting for Fecha. 25 | */ 26 | export interface LogOptions { 27 | console?: boolean; 28 | level?: LogLevels; 29 | name: string; 30 | stream?: Writable; 31 | timestamp?: boolean | string; 32 | newline?: boolean; 33 | } 34 | 35 | export default class Log implements LogOptions { 36 | /** Levels enum used to determine when to log, `none` means nothing will be logged. */ 37 | public static Levels: typeof LogLevels = LogLevels; 38 | 39 | /** Whether to write to console or not. */ 40 | public console: boolean = true; 41 | /** The maximum level to output. */ 42 | public level: LogLevels; 43 | /** Determines the filename from `logs/${name}.log` to output as. */ 44 | public readonly name: string; 45 | /** The stream to write to. */ 46 | public readonly stream: Writable; 47 | /** The timestamp format using [Fecha's formatting tokens](https://www.npmjs.com/package/fecha#formatting-tokens) */ 48 | public timestamp: string; 49 | 50 | /** The filepath of the stream. */ 51 | private readonly _filepath: string; 52 | 53 | /** Whether to log with newline or not. */ 54 | public readonly newline: boolean; 55 | 56 | /** 57 | * Creates a new instance of Log. 58 | * @param {LogOptions} options `LogOptions` Options to define Log's behaviour. 59 | * @returns {Log} `Log` An instance of Log. 60 | */ 61 | constructor(options: LogOptions) { 62 | // Set the required options first. 63 | this.name = options.name; 64 | this.newline = options.newline || Boolean(process.env.LOG_NEWLINE) || false; 65 | 66 | // Use the name to determine the path for the stream. 67 | this._filepath = `logs/${this.name}.log`; 68 | 69 | this.console = options.console || true; 70 | 71 | if (typeof options.level === "undefined") { 72 | this.level = 73 | process.env.NODE_ENV === "development" 74 | ? LogLevels.debug 75 | : LogLevels.info; 76 | } else { 77 | this.level = options.level; 78 | } 79 | 80 | if (typeof options.stream === "undefined") { 81 | this.stream = rfs.createStream(this._filepath, { 82 | compress: "gzip", 83 | interval: "5d", 84 | maxFiles: 6, 85 | }); 86 | } else { 87 | this.stream = options.stream; 88 | } 89 | 90 | if (typeof options.timestamp === "string") { 91 | // If the passed timestamp is a string use that as the format. 92 | this.timestamp = options.timestamp; 93 | } else if (typeof options.timestamp === "undefined" || options.timestamp) { 94 | // If the passed timestamp is unset or `true`, use the default format. 95 | // this.timestamp = "YYYY-MM-DD HH:mm:ss,SSS"; 96 | this.timestamp = "HH:mm:ss"; 97 | } else { 98 | // Otherwise disable the timestamp. 99 | this.timestamp = ""; 100 | } 101 | } 102 | 103 | /** 104 | * Creates a new instance of Log with the properties of the original. 105 | * @param {Partial?} options Options to override the original's. 106 | * @returns {Log} The new instance of Log. 107 | */ 108 | public extend(options?: Partial): Log { 109 | return new Log({ 110 | ...this, 111 | ...options, 112 | }); 113 | } 114 | 115 | /** 116 | * Logs a message with the `Error` level. 117 | * @param {string} message `string` The message to log. 118 | * @returns {string | undefined} Returns `string` if something was logged and `undefined` if the Log instance level was lower than the `Error` level. 119 | */ 120 | public error(message: any): string | undefined { 121 | if (this.level < LogLevels.error) { 122 | return; 123 | } 124 | 125 | this._writeToConsole(message, LogLevels.error); 126 | return this._writeToStream(message, LogLevels.error); 127 | } 128 | 129 | /** 130 | * Logs a message with the `Warn` level. 131 | * @param {string} message `string` The message to log. 132 | * @returns {string | undefined} Returns `string` if something was logged and `undefined` if the Log instance level was lower than the `Warn` level. 133 | */ 134 | public warn(message: any): string | undefined { 135 | if (this.level < LogLevels.warn) { 136 | return; 137 | } 138 | 139 | this._writeToConsole(message, LogLevels.warn); 140 | return this._writeToStream(message, LogLevels.warn); 141 | } 142 | 143 | /** 144 | * Logs a message with the `Info` level. 145 | * @param {string} message `string` The message to log. 146 | * @returns {string | undefined} Returns `string` if something was logged and `undefined` if the Log instance level was lower than the `Info` level. 147 | */ 148 | public info(message: any): string | undefined { 149 | if (this.level < LogLevels.info) { 150 | return; 151 | } 152 | 153 | this._writeToConsole(message, LogLevels.info); 154 | return this._writeToStream(message, LogLevels.info); 155 | } 156 | 157 | /** 158 | * Logs a message with the `Debug` level. 159 | * @param {string} message `string` The message to log. 160 | * @returns {string | undefined} Returns `string` if something was logged and `undefined` if the Log instance level was lower than the `Debug` level. 161 | */ 162 | public debug(message: any): string | undefined { 163 | if (this.level < LogLevels.debug) { 164 | return; 165 | } 166 | 167 | this._writeToConsole(message, LogLevels.debug); 168 | return this._writeToStream(message, LogLevels.debug); 169 | } 170 | 171 | /** 172 | * Writes a message to the console when applicable. 173 | * @param {string} message `string` The message to write. 174 | * @param {LogLevels} level `LogLevels` The level to write. 175 | */ 176 | private _writeToConsole(message: any, level: LogLevels): void { 177 | if (this.console) { 178 | console.log(this._formatMessage(message, level, true)); 179 | } 180 | } 181 | 182 | /** 183 | * Writes a message to the instance's stream. 184 | * @param {string} message `string` The message to write. 185 | * @param {LogLevels} level `LogLevels` The level to write. 186 | * @returns {string} `string` The formatted message that was logged. 187 | */ 188 | private _writeToStream(message: string, level: LogLevels): string { 189 | message = this._formatMessage(message, level) + "\n"; 190 | this.stream.write(message); 191 | return message; 192 | } 193 | 194 | /** 195 | * Formats a message with the level and timestamp (if applicable). 196 | * @param {string} message `string` The message to be formatted. 197 | * @param {LogLevels} level `LogLevels` The level to be formatted. 198 | * @param {boolean?} forConsole `boolean | undefined` Whether to add Chalk styling for the console. 199 | * @returns {string} `string` The formatted message. 200 | */ 201 | private _formatMessage( 202 | message: string, 203 | level: LogLevels, 204 | forConsole?: boolean 205 | ): string { 206 | const levelString: string = this._getFormattedLevel(level, forConsole); 207 | const timestamp: string = this._getTimestamp(forConsole); 208 | const loggerName: string = this._getFormattedName(this.name); 209 | if (timestamp.length === 0) { 210 | return `${levelString} ${message}`; 211 | } 212 | 213 | return `${timestamp} ${levelString} ${message} - ${loggerName}${ 214 | this.newline ? "\n" : "" 215 | }`; 216 | } 217 | 218 | /** 219 | * Gets a string representation of a LogLevels' value. 220 | * @param {LogLevels} wanted `LogLevels` The wanted level to get a formatted representation of. 221 | * @param {boolean?} forConsole `boolean | undefined` Whether to add Chalk styling for the console. 222 | * @returns {string} `string` The string representation of the wanted level. 223 | */ 224 | private _getFormattedLevel(wanted: LogLevels, forConsole?: boolean): string { 225 | let level = ""; 226 | 227 | if (wanted === LogLevels.error) { 228 | level = "Error"; 229 | } else if (wanted === LogLevels.warn) { 230 | level = "Warn "; 231 | } else if (wanted === LogLevels.info) { 232 | level = "Info "; 233 | } else { 234 | level = "Debug"; 235 | } 236 | 237 | if (forConsole === true) { 238 | if (wanted === LogLevels.error) { 239 | level = chalk.black.bold.red(level); 240 | } else if (wanted === LogLevels.warn) { 241 | level = chalk.black.bold.yellow(level); 242 | } else if (wanted === LogLevels.info) { 243 | level = chalk.black.bold.green(level); 244 | } else { 245 | level = chalk.black.bold.cyan(level); 246 | } 247 | } 248 | 249 | return level; 250 | } 251 | 252 | /** 253 | * Returns a string timestamp with the current time (if applicable). 254 | * @param {boolean?} forConsole `boolean | undefined` Whether to add Chalk styling for the console. 255 | * @returns {string} `string` The string timestamp, the string will be empty if the timestamp format is empty. 256 | */ 257 | private _getTimestamp(forConsole?: boolean): string { 258 | if (this.timestamp.length === 0) { 259 | return ""; 260 | } 261 | 262 | let timestamp: string = fecha.format(new Date(), this.timestamp); 263 | if (forConsole === true) { 264 | timestamp = chalk.gray(timestamp); 265 | } 266 | 267 | return timestamp; 268 | } 269 | 270 | private _getFormattedName(name: string): string { 271 | return chalk.white.bold(name); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/utils/swap.ts: -------------------------------------------------------------------------------- 1 | import Arweave from "arweave"; 2 | import { JWKInterface } from "arweave/node/lib/wallet"; 3 | import Web3 from "web3"; 4 | 5 | const roundETH = (input: number): string => { 6 | const str = input.toString(); 7 | const amountBeforeDecimal = str.split(".")[0].length; 8 | return parseFloat(str) 9 | .toFixed(18 - amountBeforeDecimal) 10 | .toString(); 11 | }; 12 | 13 | export const sendAR = async ( 14 | input: { 15 | amount: number; 16 | target: string; 17 | order: string; 18 | match: string; 19 | }, 20 | client: Arweave, 21 | jwk: JWKInterface 22 | ): Promise => { 23 | const tx = await client.createTransaction( 24 | { 25 | target: input.target, 26 | quantity: client.ar.arToWinston(input.amount.toString()), 27 | }, 28 | jwk 29 | ); 30 | 31 | tx.addTag("Exchange", "Verto"); 32 | tx.addTag("Type", "AR-Transfer"); 33 | tx.addTag("Order", input.order); 34 | tx.addTag("Match", input.match); 35 | 36 | await client.transactions.sign(tx, jwk); 37 | await client.transactions.post(tx); 38 | 39 | return tx.id; 40 | }; 41 | 42 | export const sendETH = async ( 43 | input: { 44 | amount: number; 45 | target: string; 46 | gas: number; 47 | }, 48 | client: Web3, 49 | sign: any 50 | ): Promise => { 51 | const tx = await sign({ 52 | to: input.target, 53 | value: client.utils.toWei(roundETH(input.amount), "ether"), 54 | gas: input.gas, 55 | }); 56 | 57 | const res = await client.eth.sendSignedTransaction(tx.rawTransaction); 58 | 59 | return res.transactionHash; 60 | }; 61 | 62 | export const sendConfirmation = async ( 63 | input: { 64 | amount: string; 65 | order: string; 66 | }, 67 | client: Arweave, 68 | jwk: JWKInterface 69 | ) => { 70 | const tx = await client.createTransaction( 71 | { 72 | data: Math.random().toString().slice(-4), 73 | }, 74 | jwk 75 | ); 76 | 77 | tx.addTag("Exchange", "Verto"); 78 | tx.addTag("Type", "Confirmation"); 79 | tx.addTag("Swap", input.order); 80 | tx.addTag("Received", input.amount); 81 | 82 | await client.transactions.sign(tx, jwk); 83 | await client.transactions.post(tx); 84 | }; 85 | -------------------------------------------------------------------------------- /src/utils/tree.ts: -------------------------------------------------------------------------------- 1 | function makePrefix(key: any, last: any) { 2 | var str = last ? "└" : "├"; 3 | if (key) { 4 | str += "─ "; 5 | } else { 6 | str += "──┐"; 7 | } 8 | return str; 9 | } 10 | 11 | function filterKeys( 12 | obj: { [x: string]: any; hasOwnProperty: (arg0: string) => any }, 13 | hideFunctions: any 14 | ) { 15 | var keys = []; 16 | for (var branch in obj) { 17 | // always exclude anything in the object's prototype 18 | if (!obj.hasOwnProperty(branch)) { 19 | continue; 20 | } 21 | // ... and hide any keys mapped to functions if we've been told to 22 | if (hideFunctions && typeof obj[branch] === "function") { 23 | continue; 24 | } 25 | keys.push(branch); 26 | } 27 | return keys; 28 | } 29 | 30 | function growBranch( 31 | key: string, 32 | root: string, 33 | last: boolean, 34 | lastStates: any[], 35 | showValues: any, 36 | hideFunctions: any, 37 | callback: { (line: any): void; (arg0: string): void } 38 | ) { 39 | var line = "", 40 | index = 0, 41 | lastKey, 42 | circular: boolean = false, 43 | lastStatesCopy = lastStates.slice(0); 44 | 45 | if (lastStatesCopy.push([root, last]) && lastStates.length > 0) { 46 | // based on the "was last element" states of whatever we're nested within, 47 | // we need to append either blankness or a branch to our line 48 | lastStates.forEach((lastState: any[], idx: number) => { 49 | if (idx > 0) { 50 | line += (lastState[1] ? " " : "│") + " "; 51 | } 52 | if (!circular && lastState[0] === root) { 53 | circular = true; 54 | } 55 | }); 56 | 57 | // the prefix varies based on whether the key contains something to show and 58 | // whether we're dealing with the last element in this collection 59 | line += makePrefix(key, last) + key; 60 | 61 | // append values and the circular reference indicator 62 | showValues && typeof root !== "object" && (line += ": " + root); 63 | circular && (line += " (circular ref.)"); 64 | 65 | callback(line); 66 | } 67 | 68 | // can we descend into the next item? 69 | if (!circular && typeof root === "object") { 70 | var keys = filterKeys(root, hideFunctions); 71 | keys.forEach(function (branch) { 72 | // the last key is always printed with a different prefix, so we'll need to know if we have it 73 | lastKey = ++index === keys.length; 74 | 75 | // hold your breath for recursive action 76 | growBranch( 77 | branch, 78 | root[branch], 79 | lastKey, 80 | lastStatesCopy, 81 | showValues, 82 | hideFunctions, 83 | callback 84 | ); 85 | }); 86 | } 87 | } 88 | 89 | // asLines 90 | // -------------------- 91 | // Outputs the tree line-by-line, calling the lineCallback when each one is available. 92 | 93 | export const asLines = function ( 94 | obj: any, 95 | showValues: any, 96 | hideFunctions: any, 97 | lineCallback: any 98 | ) { 99 | /* hideFunctions and lineCallback are curried, which means we don't break apps using the older form */ 100 | var hideFunctionsArg = 101 | typeof hideFunctions !== "function" ? hideFunctions : false; 102 | growBranch( 103 | ".", 104 | obj, 105 | false, 106 | [], 107 | showValues, 108 | hideFunctionsArg, 109 | lineCallback || hideFunctions 110 | ); 111 | }; 112 | 113 | // asTree 114 | // -------------------- 115 | // Outputs the entire tree, returning it as a string with line breaks. 116 | 117 | export const asTree = function ( 118 | obj: any, 119 | showValues?: any, 120 | hideFunctions?: any 121 | ) { 122 | var tree = ""; 123 | growBranch( 124 | ".", 125 | obj, 126 | false, 127 | [], 128 | showValues, 129 | hideFunctions, 130 | function (line: string) { 131 | tree += line + "\n"; 132 | } 133 | ); 134 | return tree; 135 | }; 136 | -------------------------------------------------------------------------------- /src/workflows/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import Log from "@utils/logger"; 2 | import { Database } from "sqlite"; 3 | import { TradingPostConfig } from "@utils/config"; 4 | import { getChainAddr, init, latestTxs } from "@utils/arweave"; 5 | import { init as ethInit } from "@utils/eth"; 6 | import { genesis } from "@workflows/genesis"; 7 | import { cancel } from "@workflows/cancel"; 8 | import { ethSwap } from "@workflows/swap"; 9 | import { match } from "@workflows/match"; 10 | import Web3 from "web3"; 11 | import { getTxStore } from "@utils/database"; 12 | import Arweave from "arweave"; 13 | 14 | const log = new Log({ 15 | level: Log.Levels.debug, 16 | name: "bootstrap", 17 | }); 18 | 19 | async function getLatestTxs( 20 | client: Arweave, 21 | db: Database, 22 | addr: string, 23 | latest: { 24 | block: number; 25 | txID: string; 26 | }, 27 | ethClient: Web3, 28 | ethAddr: string, 29 | counter: number 30 | ): Promise<{ 31 | txs: { 32 | id: string; 33 | block: number; 34 | sender: { ar: string; eth?: string }; 35 | type: string; 36 | table?: string; 37 | token?: string; 38 | order?: string; 39 | arAmnt?: number; 40 | amnt?: number; 41 | rate?: number; 42 | }[]; 43 | latest: { 44 | block: number; 45 | txID: string; 46 | }; 47 | }> { 48 | const arRes = await latestTxs(client, db, addr, latest); 49 | 50 | const ethRes: { 51 | id: string; 52 | block: number; 53 | sender: { ar: string; eth?: string }; 54 | type: string; 55 | table?: string; 56 | token?: string; 57 | order?: string; 58 | arAmnt?: number; 59 | amnt?: number; 60 | rate?: number; 61 | }[] = []; 62 | if (counter == 60) { 63 | const store = await getTxStore(db); 64 | for (const entry of store) { 65 | try { 66 | const tx = await ethClient.eth.getTransaction(entry.txHash); 67 | 68 | if (tx.to !== ethAddr) { 69 | // tx is invalid 70 | await db.run(`UPDATE "TX_STORE" SET parsed = 1 WHERE txHash = ?`, [ 71 | entry.txHash, 72 | ]); 73 | } 74 | 75 | if (tx.blockNumber) { 76 | ethRes.push({ 77 | id: entry.txHash, 78 | block: tx.blockNumber, 79 | sender: { ar: entry.sender, eth: tx.from }, 80 | type: "Swap", 81 | table: entry.chain, 82 | token: entry.token, 83 | amnt: parseFloat(ethClient.utils.fromWei(tx.value, "ether")), 84 | }); 85 | await db.run(`UPDATE "TX_STORE" SET parsed = 1 WHERE txHash = ?`, [ 86 | entry.txHash, 87 | ]); 88 | } 89 | } catch (err) { 90 | await db.run(`UPDATE "TX_STORE" SET parsed = 1 WHERE txHash = ?`, [ 91 | entry.txHash, 92 | ]); 93 | } 94 | } 95 | } 96 | 97 | return { 98 | txs: arRes.txs.concat(ethRes), 99 | latest: arRes.latest, 100 | }; 101 | } 102 | 103 | export async function bootstrap( 104 | config: TradingPostConfig, 105 | db: Database, 106 | keyfile?: string, 107 | ethKeyfile?: string 108 | ) { 109 | const { client, addr, jwk } = await init(keyfile); 110 | const { client: ethClient, addr: ethAddr, sign } = await ethInit( 111 | ethKeyfile, 112 | config.genesis.chain["ETH"].node 113 | ); 114 | 115 | await genesis(client, jwk!, config.genesis); 116 | 117 | log.info("Monitoring wallets for incoming transactions..."); 118 | 119 | let latest: { 120 | block: number; 121 | txID: string; 122 | } = { 123 | block: (await client.network.getInfo()).height, 124 | txID: await client.wallets.getLastTransactionID(addr), 125 | }; 126 | 127 | let counter = 60; 128 | 129 | setInterval(async () => { 130 | const res = await getLatestTxs( 131 | client, 132 | db, 133 | addr, 134 | latest, 135 | ethClient, 136 | ethAddr, 137 | counter 138 | ); 139 | const txs = res.txs; 140 | 141 | latest = res.latest; 142 | if (counter == 60) { 143 | counter = 0; 144 | } else { 145 | counter++; 146 | } 147 | 148 | if (txs.length !== 0) { 149 | for (const tx of txs) { 150 | try { 151 | if (tx.type === "Cancel") { 152 | await cancel(client, tx.id, tx.order!, jwk!, db); 153 | } else if (tx.type === "Swap") { 154 | if (tx.table === "ETH") { 155 | if ("ETH" in config.genesis.chain) { 156 | await ethSwap( 157 | { 158 | id: tx.id, 159 | sender: tx.sender, 160 | table: tx.table, 161 | token: tx.token, 162 | arAmnt: tx.arAmnt, 163 | amnt: tx.amnt, 164 | rate: tx.rate, 165 | received: 0, 166 | }, 167 | db, 168 | client, 169 | jwk!, 170 | ethClient, 171 | sign 172 | ); 173 | } else { 174 | log.error( 175 | `Received an ETH swap.\n\t\tConsider adding support for this.` 176 | ); 177 | } 178 | } 179 | } else { 180 | if (tx.table! in config.genesis.blockedTokens) { 181 | log.error( 182 | `Token for order is blocked.\n\t\ttxID = ${tx.id}\n\t\ttype = ${ 183 | tx.type 184 | }\n\t\ttoken = ${tx.table!}` 185 | ); 186 | } else { 187 | await match(client, { ...tx, sender: tx.sender.ar }, jwk!, db); 188 | } 189 | } 190 | } catch (err) { 191 | log.error( 192 | `Failed to handle transaction.\n\t\ttxID = ${tx.id}\n\t\t${err}` 193 | ); 194 | process.exit(1); 195 | } 196 | } 197 | } 198 | }, 10000); 199 | } 200 | -------------------------------------------------------------------------------- /src/workflows/cancel.ts: -------------------------------------------------------------------------------- 1 | import Log from "@utils/logger"; 2 | import Arweave from "arweave"; 3 | import { JWKInterface } from "arweave/node/lib/wallet"; 4 | import { Database } from "sqlite"; 5 | import { query } from "@utils/gql"; 6 | import txQuery from "../queries/tx.gql"; 7 | import { getOrder } from "@utils/database"; 8 | import { readContract } from "smartweave"; 9 | 10 | const log = new Log({ 11 | level: Log.Levels.debug, 12 | name: "cancel", 13 | }); 14 | 15 | export async function cancel( 16 | client: Arweave, 17 | cancelID: string, 18 | txID: string, 19 | jwk: JWKInterface, 20 | db: Database 21 | ) { 22 | const cancelTx = ( 23 | await query({ 24 | query: txQuery, 25 | variables: { 26 | txID: cancelID, 27 | }, 28 | }) 29 | ).data.transaction; 30 | 31 | const tx = ( 32 | await query({ 33 | query: txQuery, 34 | variables: { 35 | txID, 36 | }, 37 | }) 38 | ).data.transaction; 39 | 40 | if (tx.owner.address !== cancelTx.owner.address) { 41 | log.error("Sender of cancel tx isn't owner of order."); 42 | return; 43 | } 44 | 45 | const type = tx.tags.find( 46 | (tag: { name: string; value: string }) => tag.name === "Type" 47 | ).value; 48 | const tokenTag = type === "Buy" ? "Token" : "Contract"; 49 | const token = tx.tags.find( 50 | (tag: { name: string; value: string }) => tag.name === tokenTag 51 | ).value; 52 | 53 | const order = await getOrder(db, token, txID); 54 | 55 | if (type === "Buy") { 56 | const tx = await client.createTransaction( 57 | { 58 | target: order.addr, 59 | quantity: client.ar.arToWinston(order.amnt.toString()), 60 | }, 61 | jwk 62 | ); 63 | 64 | tx.addTag("Exchange", "Verto"); 65 | tx.addTag("Type", "Cancel-AR-Transfer"); 66 | tx.addTag("Order", txID); 67 | 68 | await client.transactions.sign(tx, jwk); 69 | await client.transactions.post(tx); 70 | 71 | await db.run(`DELETE FROM "${token}" WHERE txID = "${txID}"`); 72 | 73 | log.info( 74 | "Cancelled!" + 75 | `\n\t\torder = ${txID}` + 76 | "\n" + 77 | `\n\t\tSent ${order.amnt} AR back to ${order.addr}` + 78 | `\n\t\ttxID = ${tx.id}` 79 | ); 80 | } else if (type === "Sell") { 81 | const tags = { 82 | Exchange: "Verto", 83 | Type: "Cancel-PST-Transfer", 84 | Order: txID, 85 | "App-Name": "SmartWeaveAction", 86 | "App-Version": "0.3.0", 87 | Contract: token, 88 | Input: JSON.stringify({ 89 | function: "transfer", 90 | target: order.addr, 91 | qty: order.amnt, 92 | }), 93 | }; 94 | 95 | const tx = await client.createTransaction( 96 | { 97 | target: order.addr, 98 | data: Math.random().toString().slice(-4), 99 | }, 100 | jwk 101 | ); 102 | 103 | for (const [key, value] of Object.entries(tags)) { 104 | tx.addTag(key, value.toString()); 105 | } 106 | 107 | await client.transactions.sign(tx, jwk); 108 | await client.transactions.post(tx); 109 | 110 | await db.run(`DELETE FROM "${token}" WHERE txID = "${txID}"`); 111 | 112 | const ticker = (await readContract(client, token)).ticker; 113 | log.info( 114 | "Cancelled!" + 115 | `\n\t\torder = ${txID}` + 116 | "\n" + 117 | `\n\t\tSent ${order.amnt} ${ticker} back to ${order.addr}` + 118 | `\n\t\ttxID = ${tx.id}` 119 | ); 120 | } else { 121 | log.error("Invalid order type."); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/workflows/genesis.ts: -------------------------------------------------------------------------------- 1 | import Log from "@utils/logger"; 2 | import Arweave from "arweave"; 3 | import { JWKInterface } from "arweave/node/lib/wallet"; 4 | import { GenesisConfig } from "@utils/config"; 5 | import { readContract } from "smartweave"; 6 | import CONSTANTS from "../utils/constants.yml"; 7 | import { query } from "@utils/gql"; 8 | import genesisQuery from "../queries/genesis.gql"; 9 | import { deepStrictEqual } from "assert"; 10 | 11 | const log = new Log({ 12 | level: Log.Levels.debug, 13 | name: "genesis", 14 | }); 15 | 16 | async function sendGenesis( 17 | client: Arweave, 18 | jwk: JWKInterface, 19 | config: GenesisConfig 20 | ) { 21 | const genesisTx = await client.createTransaction( 22 | { 23 | data: JSON.stringify(config), 24 | target: CONSTANTS.exchangeWallet, 25 | }, 26 | jwk 27 | ); 28 | 29 | genesisTx.addTag("Exchange", "Verto"); 30 | genesisTx.addTag("Type", "Genesis"); 31 | genesisTx.addTag("Content-Type", "application/json"); 32 | 33 | await client.transactions.sign(genesisTx, jwk); 34 | await client.transactions.post(genesisTx); 35 | 36 | log.info(`Sent genesis transaction.\n\t\ttxID = ${genesisTx.id}`); 37 | } 38 | 39 | interface Vault { 40 | balance: number; 41 | start: number; 42 | end: number; 43 | } 44 | 45 | export async function genesis( 46 | client: Arweave, 47 | jwk: JWKInterface, 48 | config: GenesisConfig 49 | ) { 50 | const addr = await client.wallets.jwkToAddress(jwk); 51 | 52 | const vault = (await readContract(client, CONSTANTS.exchangeContractSrc)) 53 | .vault; 54 | let stake = 0; 55 | if (addr in vault) { 56 | const height = (await client.network.getInfo()).height; 57 | const filtered = vault[addr].filter((a: Vault) => height < a.end); 58 | 59 | stake += filtered 60 | .map((a: Vault) => a.balance) 61 | .reduce((a: number, b: number) => a + b, 0); 62 | } 63 | 64 | if (stake <= 0) { 65 | log.error( 66 | "Stake value is <= 0." + 67 | "\n\t\tYou need to stake some tokens to be an eligible trading post." + 68 | `\n\t\tSee https://community.xyz/#${CONSTANTS.exchangeContractSrc}/vault` + 69 | "\n\t\tto stake your tokens." 70 | ); 71 | process.exit(1); 72 | } 73 | 74 | const possibleGenesis = ( 75 | await query({ 76 | query: genesisQuery, 77 | variables: { 78 | addr, 79 | exchange: CONSTANTS.exchangeWallet, 80 | }, 81 | }) 82 | ).data.transactions.edges; 83 | 84 | if (possibleGenesis.length === 1) { 85 | log.info( 86 | `Found genesis transaction.\n\t\ttxID = ${possibleGenesis[0].node.id}` 87 | ); 88 | 89 | const currentConfig = JSON.parse( 90 | ( 91 | await client.transactions.getData(possibleGenesis[0].node.id, { 92 | decode: true, 93 | string: true, 94 | }) 95 | ).toString() 96 | ); 97 | 98 | try { 99 | deepStrictEqual(currentConfig, config); 100 | } catch { 101 | log.info( 102 | "Local config does not match latest genesis config.\n\t\tSending new genesis transaction ..." 103 | ); 104 | await sendGenesis(client, jwk, config); 105 | } 106 | } else { 107 | log.info("Sending genesis transaction ..."); 108 | await sendGenesis(client, jwk, config); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/workflows/match.ts: -------------------------------------------------------------------------------- 1 | import Log from "@utils/logger"; 2 | import Arweave from "arweave"; 3 | import { JWKInterface } from "arweave/node/lib/wallet"; 4 | import { Database } from "sqlite"; 5 | import { 6 | OrderInstance, 7 | saveOrder, 8 | getSellOrders, 9 | getBuyOrders, 10 | } from "@utils/database"; 11 | import { readContract } from "smartweave"; 12 | 13 | const log = new Log({ 14 | level: Log.Levels.debug, 15 | name: "match", 16 | }); 17 | 18 | async function sendConfirmation( 19 | client: Arweave, 20 | txID: string, 21 | received: string, 22 | jwk: JWKInterface 23 | ) { 24 | const confirmationTx = await client.createTransaction( 25 | { 26 | data: Math.random().toString().slice(-4), 27 | }, 28 | jwk 29 | ); 30 | 31 | confirmationTx.addTag("Exchange", "Verto"); 32 | confirmationTx.addTag("Type", "Confirmation"); 33 | confirmationTx.addTag("Match", txID); 34 | confirmationTx.addTag("Received", received); 35 | 36 | await client.transactions.sign(confirmationTx, jwk); 37 | await client.transactions.post(confirmationTx); 38 | } 39 | 40 | export async function match( 41 | client: Arweave, 42 | tx: { 43 | id: string; 44 | sender: string; 45 | type: string; 46 | table?: string; 47 | arAmnt?: number; 48 | amnt?: number; 49 | rate?: number; 50 | }, 51 | jwk: JWKInterface, 52 | db: Database 53 | ) { 54 | const type = tx.type; 55 | 56 | let amnt = type === "Buy" ? tx.arAmnt! : tx.amnt!; 57 | let received = 0; 58 | const token = tx.table!; 59 | const ticker = (await readContract(client, token)).ticker; 60 | 61 | let rate = tx.rate; 62 | 63 | log.info(`Received order.\n\t\ttxID = ${tx.id}\n\t\ttype = ${type}`); 64 | 65 | if (type === "Buy" && (await getSellOrders(db, token)).length === 0) { 66 | let returnTx; 67 | 68 | returnTx = await client.createTransaction( 69 | { 70 | target: tx.sender, 71 | quantity: client.ar.arToWinston(amnt.toString()), 72 | }, 73 | jwk 74 | ); 75 | 76 | const fee = parseFloat( 77 | client.ar.winstonToAr( 78 | await client.transactions.getPrice( 79 | parseFloat(returnTx.data_size), 80 | returnTx.target 81 | ) 82 | ) 83 | ); 84 | 85 | returnTx = await client.createTransaction( 86 | { 87 | target: tx.sender, 88 | quantity: client.ar.arToWinston((amnt - fee).toString()), 89 | }, 90 | jwk 91 | ); 92 | 93 | returnTx.addTag("Exchange", "Verto"); 94 | returnTx.addTag("Type", "Buy-Return"); 95 | returnTx.addTag("Order", tx.id); 96 | await client.transactions.sign(returnTx, jwk); 97 | await client.transactions.post(returnTx); 98 | 99 | log.info( 100 | `Returned buy order.\n\t\torder = ${tx.id}\n\t\ttxID = ${returnTx.id}` 101 | ); 102 | return; 103 | } 104 | 105 | const tokenEntry: OrderInstance = { 106 | txID: tx.id, 107 | amnt, 108 | rate, 109 | addr: tx.sender, 110 | // @ts-ignore 111 | type, 112 | createdAt: new Date(), 113 | received, 114 | }; 115 | await saveOrder(db, token, tokenEntry); 116 | 117 | if (type === "Buy") { 118 | const orders = await getSellOrders(db, token); 119 | for (const order of orders) { 120 | if (!order.rate) continue; 121 | const pstAmount = Math.floor(amnt * order.rate); 122 | 123 | if (pstAmount === 0) { 124 | let returnTx; 125 | 126 | returnTx = await client.createTransaction( 127 | { 128 | target: tx.sender, 129 | quantity: client.ar.arToWinston(amnt.toString()), 130 | }, 131 | jwk 132 | ); 133 | 134 | const fee = parseFloat( 135 | client.ar.winstonToAr( 136 | await client.transactions.getPrice( 137 | parseFloat(returnTx.data_size), 138 | returnTx.target 139 | ) 140 | ) 141 | ); 142 | 143 | returnTx = await client.createTransaction( 144 | { 145 | target: tx.sender, 146 | quantity: client.ar.arToWinston((amnt - fee).toString()), 147 | }, 148 | jwk 149 | ); 150 | 151 | returnTx.addTag("Exchange", "Verto"); 152 | returnTx.addTag("Type", "Buy-Return"); 153 | returnTx.addTag("Order", tx.id); 154 | await client.transactions.sign(returnTx, jwk); 155 | await client.transactions.post(returnTx); 156 | 157 | await db.run(`DELETE FROM "${token}" WHERE txID = ?`, [tx.id]); 158 | 159 | log.info( 160 | `Returned buy order.\n\t\torder = ${tx.id}\n\t\ttxID = ${returnTx.id}` 161 | ); 162 | return; 163 | } 164 | 165 | if (order.amnt >= pstAmount) { 166 | const arTx = await client.createTransaction( 167 | { 168 | target: order.addr, 169 | quantity: client.ar.arToWinston(amnt.toString()), 170 | }, 171 | jwk 172 | ); 173 | arTx.addTag("Exchange", "Verto"); 174 | arTx.addTag("Type", "AR-Transfer"); 175 | arTx.addTag("Order", order.txID); 176 | arTx.addTag("Match", tx.id); 177 | await client.transactions.sign(arTx, jwk); 178 | await client.transactions.post(arTx); 179 | 180 | const tags = { 181 | Exchange: "Verto", 182 | Type: "PST-Transfer", 183 | Order: tx.id, 184 | Match: order.txID, 185 | "App-Name": "SmartWeaveAction", 186 | "App-Version": "0.3.0", 187 | Contract: token, 188 | Input: JSON.stringify({ 189 | function: "transfer", 190 | target: tx.sender, 191 | qty: pstAmount, 192 | }), 193 | }; 194 | const pstTx = await client.createTransaction( 195 | { 196 | target: tx.sender, 197 | data: Math.random().toString().slice(-4), 198 | }, 199 | jwk 200 | ); 201 | for (const [key, value] of Object.entries(tags)) { 202 | pstTx.addTag(key, value.toString()); 203 | } 204 | await client.transactions.sign(pstTx, jwk); 205 | await client.transactions.post(pstTx); 206 | 207 | log.info( 208 | "Matched!" + 209 | `\n\t\tSent ${amnt} AR to ${order.addr}` + 210 | `\n\t\ttxID = ${arTx.id}` + 211 | "\n" + 212 | `\n\t\tSent ${pstAmount} ${ticker} to ${tx.sender}` + 213 | `\n\t\ttxID = ${pstTx.id}` 214 | ); 215 | 216 | if (order.amnt === pstAmount) { 217 | /** 218 | * Delete an order. 219 | * NOTE: Table names are not subject to sql injections. 220 | */ 221 | await db.run(`DELETE FROM "${token}" WHERE txID = ?`, [order.txID]); 222 | await sendConfirmation( 223 | client, 224 | order.txID, 225 | `${order.received + amnt} AR`, 226 | jwk 227 | ); 228 | } else { 229 | /** 230 | * Update an order. 231 | * NOTE: Table names are not subject to sql injections 232 | */ 233 | await db.run( 234 | `UPDATE "${token}" SET amnt = ?, received = ? WHERE txID = ?`, 235 | [order.amnt - pstAmount, order.received + amnt, order.txID] 236 | ); 237 | } 238 | /** 239 | * Delete an order. 240 | * NOTE: Table names are not subject to sql injections 241 | */ 242 | await db.run(`DELETE FROM "${token}" WHERE txID = ?`, [tx.id]); 243 | await sendConfirmation( 244 | client, 245 | tx.id, 246 | `${received + pstAmount} ${ticker}`, 247 | jwk 248 | ); 249 | 250 | return; 251 | } else { 252 | const arTx = await client.createTransaction( 253 | { 254 | target: order.addr, 255 | quantity: client.ar.arToWinston( 256 | (order.amnt / order.rate).toString() 257 | ), 258 | }, 259 | jwk 260 | ); 261 | arTx.addTag("Exchange", "Verto"); 262 | arTx.addTag("Type", "AR-Transfer"); 263 | arTx.addTag("Order", order.txID); 264 | arTx.addTag("Match", tx.id); 265 | await client.transactions.sign(arTx, jwk); 266 | await client.transactions.post(arTx); 267 | 268 | const tags = { 269 | Exchange: "Verto", 270 | Type: "PST-Transfer", 271 | Order: tx.id, 272 | Match: order.txID, 273 | "App-Name": "SmartWeaveAction", 274 | "App-Version": "0.3.0", 275 | Contract: token, 276 | Input: JSON.stringify({ 277 | function: "transfer", 278 | target: tx.sender, 279 | qty: Math.floor(order.amnt), 280 | }), 281 | }; 282 | const pstTx = await client.createTransaction( 283 | { 284 | target: tx.sender, 285 | data: Math.random().toString().slice(-4), 286 | }, 287 | jwk 288 | ); 289 | for (const [key, value] of Object.entries(tags)) { 290 | pstTx.addTag(key, value.toString()); 291 | } 292 | await client.transactions.sign(pstTx, jwk); 293 | await client.transactions.post(pstTx); 294 | 295 | log.info( 296 | "Matched!" + 297 | `\n\t\tSent ${order.amnt / order.rate} AR to ${order.addr}` + 298 | `\n\t\ttxID = ${arTx.id}` + 299 | "\n" + 300 | `\n\t\tSent ${Math.floor(order.amnt)} ${ticker} to ${tx.sender}` + 301 | `\n\t\ttxID = ${pstTx.id}` 302 | ); 303 | /** 304 | * Update an order. 305 | * NOTE: Table names are not subject to sql injections 306 | */ 307 | await db.run( 308 | `UPDATE "${token}" SET amnt = ?, received = ? WHERE txID = ?`, 309 | [ 310 | amnt - order.amnt / order.rate, 311 | received + Math.floor(order.amnt), 312 | tx.id, 313 | ] 314 | ); 315 | amnt -= order.amnt / order.rate; 316 | received += Math.floor(order.amnt); 317 | /** 318 | * Delete an order. 319 | * NOTE: Table names are not subject to sql injections 320 | */ 321 | await db.run(`DELETE FROM "${token}" WHERE txID = ?`, [order.txID]); 322 | await sendConfirmation( 323 | client, 324 | order.txID, 325 | `${order.received + order.amnt / order.rate} AR`, 326 | jwk 327 | ); 328 | } 329 | } 330 | } else if (type === "Sell") { 331 | const orders = await getBuyOrders(db, token); 332 | for (const order of orders) { 333 | if (order.amnt >= amnt / rate!) { 334 | const arTx = await client.createTransaction( 335 | { 336 | target: tx.sender, 337 | quantity: client.ar.arToWinston((amnt / rate!).toString()), 338 | }, 339 | jwk 340 | ); 341 | arTx.addTag("Exchange", "Verto"); 342 | arTx.addTag("Type", "AR-Transfer"); 343 | arTx.addTag("Order", tx.id); 344 | arTx.addTag("Match", order.txID); 345 | await client.transactions.sign(arTx, jwk); 346 | await client.transactions.post(arTx); 347 | 348 | const tags = { 349 | Exchange: "Verto", 350 | Type: "PST-Transfer", 351 | Order: order.txID, 352 | Match: tx.id, 353 | "App-Name": "SmartWeaveAction", 354 | "App-Version": "0.3.0", 355 | Contract: token, 356 | Input: JSON.stringify({ 357 | function: "transfer", 358 | target: order.addr, 359 | qty: Math.floor(amnt), 360 | }), 361 | }; 362 | const pstTx = await client.createTransaction( 363 | { 364 | target: order.addr, 365 | data: Math.random().toString().slice(-4), 366 | }, 367 | jwk 368 | ); 369 | for (const [key, value] of Object.entries(tags)) { 370 | pstTx.addTag(key, value.toString()); 371 | } 372 | await client.transactions.sign(pstTx, jwk); 373 | await client.transactions.post(pstTx); 374 | 375 | log.info( 376 | "Matched!" + 377 | `\n\t\tSent ${amnt / rate!} AR to ${tx.sender}` + 378 | `\n\t\ttxID = ${arTx.id}` + 379 | "\n" + 380 | `\n\t\tSent ${Math.floor(amnt)} ${ticker} to ${order.addr}` + 381 | `\n\t\ttxID = ${pstTx.id}` 382 | ); 383 | 384 | if (order.amnt === amnt / rate!) { 385 | /** 386 | * Delete an order. 387 | * NOTE: Table names are not subject to sql injections 388 | */ 389 | await db.run(`DELETE FROM "${token}" WHERE txID = ?`, [order.txID]); 390 | await sendConfirmation( 391 | client, 392 | order.txID, 393 | `${order.received + Math.floor(amnt)} ${ticker}`, 394 | jwk 395 | ); 396 | } else { 397 | /** 398 | * Update an order. 399 | * NOTE: Table names are not subject to sql injections 400 | */ 401 | await db.run( 402 | `UPDATE "${token}" SET amnt = ?, received = ? WHERE txID = ?`, 403 | [ 404 | order.amnt - amnt / rate!, 405 | order.received + Math.floor(amnt), 406 | order.txID, 407 | ] 408 | ); 409 | } 410 | await db.run(`DELETE FROM "${token}" WHERE txID = ?`, [tx.id]); 411 | await sendConfirmation( 412 | client, 413 | tx.id, 414 | `${received + amnt / rate!} AR`, 415 | jwk 416 | ); 417 | 418 | return; 419 | } else { 420 | const arTx = await client.createTransaction( 421 | { 422 | target: tx.sender, 423 | quantity: client.ar.arToWinston(order.amnt.toString()), 424 | }, 425 | jwk 426 | ); 427 | arTx.addTag("Exchange", "Verto"); 428 | arTx.addTag("Type", "AR-Transfer"); 429 | arTx.addTag("Order", tx.id); 430 | arTx.addTag("Match", order.txID); 431 | await client.transactions.sign(arTx, jwk); 432 | await client.transactions.post(arTx); 433 | 434 | const tags = { 435 | Exchange: "Verto", 436 | Type: "PST-Transfer", 437 | Order: order.txID, 438 | Match: tx.id, 439 | "App-Name": "SmartWeaveAction", 440 | "App-Version": "0.3.0", 441 | Contract: token, 442 | Input: JSON.stringify({ 443 | function: "transfer", 444 | target: order.addr, 445 | qty: Math.floor(order.amnt * rate!), 446 | }), 447 | }; 448 | const pstTx = await client.createTransaction( 449 | { 450 | target: order.addr, 451 | data: Math.random().toString().slice(-4), 452 | }, 453 | jwk 454 | ); 455 | for (const [key, value] of Object.entries(tags)) { 456 | pstTx.addTag(key, value.toString()); 457 | } 458 | await client.transactions.sign(pstTx, jwk); 459 | await client.transactions.post(pstTx); 460 | 461 | log.info( 462 | "Matched!" + 463 | `\n\t\tSent ${order.amnt} AR to ${tx.sender}` + 464 | `\n\t\ttxID = ${arTx.id}` + 465 | "\n" + 466 | `\n\t\tSent ${Math.floor(order.amnt * rate!)} ${ticker} to ${ 467 | order.addr 468 | }` + 469 | `\n\t\ttxID = ${pstTx.id}` 470 | ); 471 | /** 472 | * Update an order. 473 | * NOTE: Table names are not subject to sql injections 474 | */ 475 | await db.run( 476 | `UPDATE "${token}" SET amnt = ?, received = ? WHERE txID = ?`, 477 | [amnt - Math.floor(order.amnt * rate!), received + order.amnt, tx.id] 478 | ); 479 | amnt -= Math.floor(order.amnt * rate!); 480 | received += order.amnt; 481 | /** 482 | * Delete an order. 483 | * NOTE: Table names are not subject to sql injections 484 | */ 485 | await db.run(`DELETE FROM "${token}" WHERE txID = ?`, [order.txID]); 486 | await sendConfirmation( 487 | client, 488 | order.txID, 489 | `${order.received + Math.floor(order.amnt * rate!)} ${ticker}`, 490 | jwk 491 | ); 492 | } 493 | } 494 | } else { 495 | log.error("Invalid order type."); 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /src/workflows/swap.ts: -------------------------------------------------------------------------------- 1 | import Log from "@utils/logger"; 2 | import { Database } from "sqlite"; 3 | import Arweave from "arweave"; 4 | import { JWKInterface } from "arweave/node/lib/wallet"; 5 | import Web3 from "web3"; 6 | import { OrderInstance, saveOrder, getBuyOrders } from "@utils/database"; 7 | import { sendETH, sendAR, sendConfirmation } from "@utils/swap"; 8 | import { match } from "./match"; 9 | 10 | const log = new Log({ 11 | level: Log.Levels.debug, 12 | name: "swap", 13 | }); 14 | 15 | export async function ethSwap( 16 | tx: { 17 | id: string; 18 | sender: { ar: string; eth?: string }; 19 | table: string; 20 | token?: string; 21 | arAmnt?: number; 22 | amnt?: number; 23 | rate?: number; 24 | received: number; 25 | }, 26 | db: Database, 27 | client: Arweave, 28 | jwk: JWKInterface, 29 | ethClient: Web3, 30 | // TODO(@johnletey): Look into the type 31 | sign: any 32 | ) { 33 | log.info(`Received swap order.\n\t\ttxID = ${tx.id}`); 34 | 35 | if (tx.arAmnt) { 36 | // AR Incoming 37 | // Not recursive 38 | // Save to DB 39 | 40 | if (tx.sender.eth) { 41 | const swapEntry: OrderInstance = { 42 | txID: tx.id, 43 | amnt: tx.arAmnt, 44 | rate: tx.rate, 45 | addr: tx.sender.eth, 46 | type: "Buy", 47 | createdAt: new Date(), 48 | received: 0, 49 | }; 50 | await saveOrder(db, tx.table, swapEntry); 51 | } else { 52 | const returnTx = await client.createTransaction( 53 | { 54 | target: tx.sender.ar, 55 | quantity: client.ar.arToWinston(tx.arAmnt.toString()), 56 | }, 57 | jwk 58 | ); 59 | 60 | returnTx.addTag("Exchange", "Verto"); 61 | returnTx.addTag("Type", "Swap-Return"); 62 | returnTx.addTag("Order", tx.id); 63 | 64 | await client.transactions.sign(returnTx, jwk); 65 | await client.transactions.post(returnTx); 66 | 67 | return; 68 | } 69 | } 70 | 71 | if (tx.amnt) { 72 | // ETH Incoming 73 | // Recursive 74 | let amount = tx.amnt; 75 | 76 | // Find first order in orderbook 77 | const orders = await getBuyOrders(db, tx.table); 78 | if (orders.length === 0) { 79 | const gasPrice = parseFloat( 80 | ethClient.utils.fromWei(await ethClient.eth.getGasPrice(), "ether") 81 | ); 82 | const gas = await ethClient.eth.estimateGas({ 83 | to: tx.sender.eth, 84 | }); 85 | 86 | const ethHash = await sendETH( 87 | { amount: amount - gas * gasPrice, target: tx.sender.eth!, gas }, 88 | ethClient, 89 | sign 90 | ); 91 | 92 | log.info( 93 | `Returned swap order.\n\t\torder = ${tx.id}\n\t\ttxID = ${ethHash}` 94 | ); 95 | return; 96 | } 97 | const order = orders[0]; 98 | 99 | // Calculate gas fee to send 100 | const gasPrice = parseFloat( 101 | ethClient.utils.fromWei(await ethClient.eth.getGasPrice(), "ether") 102 | ); 103 | const gas = await ethClient.eth.estimateGas({ 104 | to: order.addr, 105 | }); 106 | 107 | // Subtract gas fee from incoming ETH amount (gETH) 108 | amount -= gas * gasPrice; 109 | 110 | if (amount < 0) { 111 | const returnTx = await client.createTransaction( 112 | { 113 | data: Math.random().toString().slice(-4), 114 | }, 115 | jwk 116 | ); 117 | 118 | returnTx.addTag("Exchange", "Verto"); 119 | returnTx.addTag("Type", "Swap-Return"); 120 | returnTx.addTag( 121 | "Message", 122 | "When executed, the Ethereum gas price was higher than the (remaining) order amount." 123 | ); 124 | returnTx.addTag("Order", tx.id); 125 | 126 | await client.transactions.sign(returnTx, jwk); 127 | await client.transactions.post(returnTx); 128 | 129 | log.info( 130 | `Returned swap order due to gas prices.\n\t\torder = ${tx.id}\n\t\ttxID = ${returnTx.id}` 131 | ); 132 | return; 133 | } 134 | 135 | // Match 136 | if (amount === order.amnt * order.rate!) { 137 | // if gETH === order 138 | 139 | // Remove order from DB 140 | await db.run(`DELETE FROM "${tx.table}" WHERE txID = ?`, [order.txID]); 141 | 142 | // Send ETH/AR in corresponding locations & confirmation transactions 143 | const ethHash = await sendETH( 144 | { amount, target: order.addr, gas }, 145 | ethClient, 146 | sign 147 | ); 148 | const arHash = await sendAR( 149 | { 150 | amount: amount / order.rate!, 151 | target: tx.sender.ar, 152 | order: tx.id, 153 | match: order.txID, 154 | }, 155 | client, 156 | jwk 157 | ); 158 | 159 | log.info( 160 | "Matched!" + 161 | `\n\t\tSent ${amount / order.rate!} AR to ${tx.sender.ar}` + 162 | `\n\t\ttxID = ${arHash}` + 163 | "\n" + 164 | `\n\t\tSent ${amount} ${tx.table} to ${order.addr}` + 165 | `\n\t\ttxID = ${ethHash}` 166 | ); 167 | 168 | if (tx.token) { 169 | await match( 170 | client, 171 | { 172 | id: tx.id, 173 | sender: tx.sender.ar, 174 | type: "Buy", 175 | table: tx.token, 176 | arAmnt: tx.received + amount / order.rate!, 177 | }, 178 | jwk, 179 | db 180 | ); 181 | } else { 182 | await sendConfirmation( 183 | { amount: `${tx.received + amount / order.rate!} AR`, order: tx.id }, 184 | client, 185 | jwk 186 | ); 187 | } 188 | await sendConfirmation( 189 | { amount: `${order.received + amount} ${tx.table}`, order: order.txID }, 190 | client, 191 | jwk 192 | ); 193 | 194 | // DONE 195 | return; 196 | } 197 | if (amount < order.amnt * order.rate!) { 198 | // if gETH < order 199 | 200 | // Subtract gETH amount from order (AKA increment matched amount) 201 | await db.run( 202 | `UPDATE "${tx.table}" SET amnt = ?, received = ? WHERE txID = ?`, 203 | [order.amnt - amount / order.rate!, order.received + amount, order.txID] 204 | ); 205 | 206 | // Send ETH/AR in corresponding locations & confirmation transactions 207 | const ethHash = await sendETH( 208 | { amount, target: order.addr, gas }, 209 | ethClient, 210 | sign 211 | ); 212 | const arHash = await sendAR( 213 | { 214 | amount: amount / order.rate!, 215 | target: tx.sender.ar, 216 | order: tx.id, 217 | match: order.txID, 218 | }, 219 | client, 220 | jwk 221 | ); 222 | 223 | log.info( 224 | "Matched!" + 225 | `\n\t\tSent ${amount / order.rate!} AR to ${tx.sender.ar}` + 226 | `\n\t\ttxID = ${arHash}` + 227 | "\n" + 228 | `\n\t\tSent ${amount} ${tx.table} to ${order.addr}` + 229 | `\n\t\ttxID = ${ethHash}` 230 | ); 231 | 232 | if (tx.token) { 233 | await match( 234 | client, 235 | { 236 | id: tx.id, 237 | sender: tx.sender.ar, 238 | type: "Buy", 239 | table: tx.token, 240 | arAmnt: tx.received + amount / order.rate!, 241 | }, 242 | jwk, 243 | db 244 | ); 245 | } else { 246 | await sendConfirmation( 247 | { amount: `${tx.received + amount / order.rate!} AR`, order: tx.id }, 248 | client, 249 | jwk 250 | ); 251 | } 252 | 253 | // DONE 254 | return; 255 | } 256 | if (amount > order.amnt * order.rate!) { 257 | // if gETH > order 258 | 259 | // Remove order from DB 260 | await db.run(`DELETE FROM "${tx.table}" WHERE txID = ?`, [order.txID]); 261 | 262 | // Send ETH/AR in corresponding locations & confirmation transactions 263 | const ethHash = await sendETH( 264 | { amount: order.amnt * order.rate!, target: order.addr, gas }, 265 | ethClient, 266 | sign 267 | ); 268 | const arHash = await sendAR( 269 | { 270 | amount: order.amnt, 271 | target: tx.sender.ar, 272 | order: tx.id, 273 | match: order.txID, 274 | }, 275 | client, 276 | jwk 277 | ); 278 | 279 | log.info( 280 | "Matched!" + 281 | `\n\t\tSent ${order.amnt} AR to ${tx.sender.ar}` + 282 | `\n\t\ttxID = ${arHash}` + 283 | "\n" + 284 | `\n\t\tSent ${order.amnt * order.rate!} ${tx.table} to ${ 285 | order.addr 286 | }` + 287 | `\n\t\ttxID = ${ethHash}` 288 | ); 289 | 290 | await sendConfirmation( 291 | { 292 | amount: `${order.received + order.amnt * order.rate!} ${tx.table}`, 293 | order: order.txID, 294 | }, 295 | client, 296 | jwk 297 | ); 298 | 299 | // Call function again with updated gETH amount 300 | await ethSwap( 301 | { 302 | ...tx, 303 | amnt: tx.amnt - order.amnt * order.rate! - gas * gasPrice, 304 | received: tx.received + order.amnt, 305 | }, 306 | db, 307 | client, 308 | jwk, 309 | ethClient, 310 | sign 311 | ); 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | ## Verto Test Suite 2 | 3 | Source inside this directory contains the test suite for Verto's trading post containing unit tests with Mocha & integration tests with Golang. 4 | 5 | ### Unit tests 6 | 7 | Unit tests are for the testing the core functionality of the trading posts individually. It contains tests for database, logger, arweave handler and other internal utils. 8 | 9 | ### Integration tests 10 | 11 | Designed to test the trading post by simulating a real-life non-node environment. It is automated via Go to avoid any kind of source manipulation and testing end-to-end. It _will_ contain tests for installers, CLI, starting the trading post, sending orders, retrieving order book and gracefully shutting down the trading post without data/db corruption. 12 | -------------------------------------------------------------------------------- /test/api.test.ts: -------------------------------------------------------------------------------- 1 | import { initAPI } from "../src/api/index"; 2 | import supertest, { SuperTest, Test } from "supertest"; 3 | let server: any; 4 | let request: SuperTest; 5 | 6 | describe("API tests", () => { 7 | it("Start server", (done) => { 8 | server = initAPI( 9 | "http://example.com", 10 | "localhost", 11 | 8080, 12 | undefined, 13 | false 14 | ).listen(); 15 | request = supertest(server); 16 | done(); 17 | }); 18 | it("Test server response", (done) => { 19 | request 20 | .get("/ping") 21 | .expect(200) 22 | .end((err, res) => { 23 | if (err) return done(err); 24 | done(); 25 | }); 26 | }); 27 | // it("Test order book endpoint", (done) => { 28 | // request 29 | // .get("/orders") 30 | // .expect(200) 31 | // .end((err, res) => { 32 | // if (err) return done(err); 33 | // done(); 34 | // }); 35 | // }); 36 | it("Shutdown server", (done) => { 37 | server.close(); 38 | done(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/commands.test.ts: -------------------------------------------------------------------------------- 1 | import OrdersCommand from "../src/commands/orders"; 2 | 3 | describe("Test `verto orders`", () => { 4 | it("Retrieve order book", async () => { 5 | return await OrdersCommand({ 6 | config: "verto.config.example.json", 7 | }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/config.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createConfig, 3 | TradingPostConfig, 4 | loadConfig, 5 | validateConfig, 6 | } from "../src/utils/config"; 7 | import { assert, expect, should } from "chai"; 8 | import sinon from "sinon"; 9 | 10 | let testConfiguration: TradingPostConfig; 11 | 12 | describe("Config tests", () => { 13 | it("Assign configuration", (done) => { 14 | testConfiguration = { 15 | genesis: { 16 | blockedTokens: [ 17 | "usjm4PCxUd5mtaon7zc97-dt-3qf67yPyqgzLnLqk5A", 18 | "FcM-QQpfcD0xTTzr8u4Su9QCgcvRx_JH4JSCQoFi6Ck", 19 | ], 20 | chain: { 21 | ETH: { 22 | addr: "", 23 | }, 24 | }, 25 | tradeFee: 0.01, 26 | publicURL: "https://example.com/", 27 | version: "0.2.0", 28 | }, 29 | database: "./db.db", 30 | api: { 31 | port: 8080, 32 | host: "localhost", 33 | }, 34 | }; 35 | assert(testConfiguration); 36 | done(); 37 | }); 38 | it("Generate configuration", async () => { 39 | return await createConfig( 40 | "./test_artifacts/verto.config.json", 41 | testConfiguration 42 | ); 43 | }); 44 | it("Read configuration", async () => { 45 | const config: TradingPostConfig = await loadConfig( 46 | "./test_artifacts/verto.config.json" 47 | ); 48 | assert(config, "Failed to assert file configuration"); 49 | expect(config).to.deep.equals( 50 | testConfiguration, 51 | "File configuration does not match with default" 52 | ); 53 | return; 54 | }); 55 | }); 56 | 57 | describe("Config validation", () => { 58 | it("Wrap process exit", (done) => { 59 | sinon.stub(process, "exit"); 60 | done(); 61 | }); 62 | it("Null checks", (done) => { 63 | let cloneConfig = testConfiguration; 64 | /** 65 | * The below ignore directive is used to simulate real-life runtime enviornment 66 | */ 67 | // @ts-ignore 68 | cloneConfig.genesis.blockedTokens.push(null); 69 | validateConfig(cloneConfig); 70 | // @ts-ignore 71 | assert(process.exit.isSinonProxy, "Faking process exit failed"); 72 | // @ts-ignore 73 | assert(process.exit.called, "process.exit is never called"); 74 | // @ts-ignore 75 | assert(process.exit.calledWith(1), "process.exit code is not 1"); 76 | done(); 77 | }); 78 | 79 | it("Trade fee check", (done) => { 80 | let cloneConfig = testConfiguration; 81 | /** 82 | * The below ignore directive is used to simulate real-life runtime enviornment 83 | */ 84 | // @ts-ignore 85 | cloneConfig.genesis.tradeFee = "some_string"; 86 | validateConfig(cloneConfig); 87 | // @ts-ignore 88 | assert(process.exit.isSinonProxy, "Faking process exit failed"); 89 | // @ts-ignore 90 | assert(process.exit.called, "process.exit is never called"); 91 | // @ts-ignore 92 | assert(process.exit.calledWith(1), "process.exit code is not 1"); 93 | done(); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/database.test.ts: -------------------------------------------------------------------------------- 1 | import { init } from "../src/utils/database"; 2 | import { mkdirSync } from "fs"; 3 | 4 | mkdirSync("./test_artifacts/", { recursive: true }); 5 | 6 | describe("Database tests", () => { 7 | it("Init database", async () => { 8 | return await init("./test_artifacts/db.db"); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/logger.test.ts: -------------------------------------------------------------------------------- 1 | import Logger from "../src/utils/logger"; 2 | import { assert } from "chai"; 3 | 4 | let log: Logger; 5 | 6 | describe("Logger tests", () => { 7 | it("Create logger instance", (done) => { 8 | log = new Logger({ 9 | name: "test", 10 | level: Logger.Levels.debug, 11 | }); 12 | assert(log); 13 | done(); 14 | }); 15 | it("Print debug message", (done) => { 16 | log.debug("This is a debug message"); 17 | done(); 18 | }); 19 | it("Print info message", (done) => { 20 | log.info("This is a info message"); 21 | done(); 22 | }); 23 | it("Print warning message", (done) => { 24 | log.warn("This is a warning message"); 25 | done(); 26 | }); 27 | it("Print error message", (done) => { 28 | log.error("This is a error message"); 29 | done(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "./dist", 5 | "baseUrl": "./src", 6 | "downlevelIteration": true, 7 | "esModuleInterop": true, 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "es2015", 11 | "module": "es2015", 12 | "lib": ["es2015"], 13 | "paths": { 14 | "@api/*": ["api/*"], 15 | "@endpoints/*": ["api/endpoints/*"], 16 | "@queries/*": ["queries/*"], 17 | "@utils/*": ["utils/*"], 18 | "@workflows/*": ["workflows/*"], 19 | "@commands/*": ["commands/*"] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /verto.config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/useverto/trading-post/master/verto.config.schema.json", 3 | "genesis": { 4 | "blockedTokens": [ 5 | "usjm4PCxUd5mtaon7zc97-dt-3qf67yPyqgzLnLqk5A", 6 | "FcM-QQpfcD0xTTzr8u4Su9QCgcvRx_JH4JSCQoFi6Ck" 7 | ], 8 | "tradeFee": 0.01, 9 | "publicURL": "your-trading-post-domain.com", 10 | "version": "0.3.0" 11 | }, 12 | "database": "./db.db", 13 | "api": { 14 | "port": 8080, 15 | "host": "localhost" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /verto.config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": {}, 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "$id": "https://example.com/object1599307258.json", 5 | "title": "Root", 6 | "type": "object", 7 | "required": ["genesis", "database", "api"], 8 | "properties": { 9 | "genesis": { 10 | "$id": "#root/genesis", 11 | "title": "Genesis", 12 | "type": "object", 13 | "required": ["blockedTokens", "tradeFee", "publicURL", "version"], 14 | "properties": { 15 | "blockedTokens": { 16 | "$id": "#root/genesis/blockedTokens", 17 | "title": "BlockedTokens", 18 | "type": "array", 19 | "default": [], 20 | "items": { 21 | "$id": "#root/genesis/blockedTokens/items", 22 | "title": "Items", 23 | "type": "string", 24 | "default": "", 25 | "examples": ["usjm4PCxUd5mtaon7zc97-dt-3qf67yPyqgzLnLqk5A"], 26 | "pattern": "^.*$" 27 | } 28 | }, 29 | "tradeFee": { 30 | "$id": "#root/genesis/tradeFee", 31 | "title": "Tradefee", 32 | "type": "number", 33 | "examples": [0.01], 34 | "default": 0.0 35 | }, 36 | "publicURL": { 37 | "$id": "#root/genesis/publicURL", 38 | "title": "Publicurl", 39 | "type": "string", 40 | "default": "", 41 | "examples": ["https://example.com/"], 42 | "pattern": "^.*$" 43 | }, 44 | "version": { 45 | "$id": "#root/genesis/version", 46 | "title": "Version", 47 | "type": "string", 48 | "default": "", 49 | "examples": ["0.2.0"], 50 | "pattern": "^.*$" 51 | } 52 | } 53 | }, 54 | "database": { 55 | "$id": "#root/database", 56 | "title": "Database", 57 | "type": "string", 58 | "default": "", 59 | "examples": ["./db.db"], 60 | "pattern": "^.*$" 61 | }, 62 | "api": { 63 | "$id": "#root/api", 64 | "title": "Api", 65 | "type": "object", 66 | "required": ["port", "host"], 67 | "properties": { 68 | "port": { 69 | "$id": "#root/api/port", 70 | "title": "Port", 71 | "type": "integer", 72 | "examples": [8080], 73 | "default": 0 74 | }, 75 | "host": { 76 | "$id": "#root/api/host", 77 | "title": "Host", 78 | "type": "string", 79 | "default": "", 80 | "examples": ["localhost"], 81 | "pattern": "^.*$" 82 | } 83 | } 84 | } 85 | } 86 | } 87 | --------------------------------------------------------------------------------