├── .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 |
4 |
5 |
6 |
Verto Trading Posts
7 |
8 |
9 | Everything needed to become part of the Verto Exchange Network
10 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------