├── .gitignore ├── LICENSE.MD ├── README.md ├── benchmarking ├── Move.toml └── sources │ └── main.move ├── cli ├── .gitignore ├── .vscode │ └── settings.json ├── README.md ├── jest.config.js ├── package.json ├── src │ ├── __tests__ │ │ └── config.test.ts │ ├── aptos-client.ts │ ├── benchmarks │ │ └── gas_benchmarks.ts │ ├── config.ts │ ├── index.ts │ ├── market.ts │ ├── test-coins.ts │ ├── usdf.ts │ └── utils │ │ ├── module-publish-utils.ts │ │ ├── module-testing-utils.ts │ │ ├── move-file-utils.ts │ │ ├── transaction-utils.ts │ │ └── types.ts ├── tsconfig.json └── yarn.lock ├── contract ├── Move.toml ├── README.md ├── output └── sources │ ├── market.move │ ├── platform.move │ ├── test │ ├── coin_test_helpers.move │ ├── test_coins.move │ └── test_utils.move │ └── token.move ├── scripts ├── aptos-ascii ├── install-hooks.sh ├── pre-commit.sh └── run-tests.sh └── www ├── README.md ├── components ├── Button.tsx ├── Container.tsx └── Navigation.tsx ├── next-env.d.ts ├── next-seo.config.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx └── index.tsx ├── postcss.config.js ├── public ├── alchemy.svg ├── assets │ └── og-cover-photo.png ├── discord.svg ├── docs.svg ├── favicon.ico ├── fonts │ ├── euclid-circular-a-bold-italic.woff2 │ ├── euclid-circular-a-bold.woff2 │ ├── euclid-circular-a-italic.woff2 │ ├── euclid-circular-a-light-italic.woff2 │ ├── euclid-circular-a-light.woff2 │ ├── euclid-circular-a-medium-italic.woff2 │ ├── euclid-circular-a-medium.woff2 │ ├── euclid-circular-a-regular.woff2 │ ├── euclid-circular-a-semibold-italic.woff2 │ └── euclid-circular-a-semibold.woff2 ├── github.svg ├── sand.png └── twitter.svg ├── styles └── globals.css ├── tailwind.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/react,macos,nextjs 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=react,macos,nextjs 3 | 4 | .idea 5 | 6 | ### macOS ### 7 | # General 8 | .DS_Store 9 | .AppleDouble 10 | .LSOverride 11 | 12 | # Icon must end with two \r 13 | Icon 14 | 15 | # Dev Environment 16 | .idea 17 | 18 | # Thumbnails 19 | ._* 20 | 21 | # Files that might appear in the root of a volume 22 | .DocumentRevisions-V100 23 | .fseventsd 24 | .Spotlight-V100 25 | .TemporaryItems 26 | .Trashes 27 | .VolumeIcon.icns 28 | .com.apple.timemachine.donotpresent 29 | 30 | # Directories potentially created on remote AFP share 31 | .AppleDB 32 | .AppleDesktop 33 | Network Trash Folder 34 | Temporary Itemsi 35 | .apdisk 36 | 37 | ### macOS Patch ### 38 | # iCloud generated files 39 | *.icloud 40 | 41 | ### NextJS ### 42 | # Dependencies 43 | /node_modules 44 | /.pnp 45 | .pnp.js 46 | 47 | # Testing 48 | /coverage 49 | 50 | # Next.js 51 | .next 52 | /out/ 53 | 54 | # Production 55 | /build 56 | 57 | # Misc 58 | *.pem 59 | 60 | # Debug 61 | npm-debug.log* 62 | yarn-debug.log* 63 | yarn-error.log* 64 | .pnpm-debug.log* 65 | 66 | # Local env files 67 | .env.local 68 | .env.development.local 69 | .env.test.local 70 | .env.production.local 71 | 72 | # Vercel 73 | .vercel 74 | 75 | ### react ### 76 | .DS_* 77 | *.log 78 | logs 79 | **/*.backup.* 80 | **/*.back.* 81 | 82 | node_modules 83 | bower_components 84 | 85 | *.sublime* 86 | 87 | psd 88 | thumb 89 | sketch 90 | 91 | # End of https://www.toptal.com/developers/gitignore/api/react,macos,nextjs 92 | 93 | 94 | benchmarking/.aptos 95 | benchmarking/build 96 | contract/.aptos 97 | contract/build 98 | -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | 3 | License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. 4 | "Business Source License" is a trademark of MariaDB Corporation Ab. 5 | 6 | ----------------------------------------------------------------------------- 7 | 8 | Parameters 9 | 10 | Licensor: Ferum Inc. 11 | 12 | Licensed Work: Ferum DEX 13 | The Licensed Work is (c) 2021 Ferum Inc 14 | 15 | ----------------------------------------------------------------------------- 16 | 17 | Terms 18 | 19 | The Licensor hereby grants you the right to copy, modify, create derivative 20 | works, redistribute, and make non-production use of the Licensed Work. The 21 | Licensor may make an Additional Use Grant, above, permitting limited 22 | production use. 23 | 24 | Effective on the Change Date, or the fourth anniversary of the first publicly 25 | available distribution of a specific version of the Licensed Work under this 26 | License, whichever comes first, the Licensor hereby grants you rights under 27 | the terms of the Change License, and the rights granted in the paragraph 28 | above terminate. 29 | 30 | If your use of the Licensed Work does not comply with the requirements 31 | currently in effect as described in this License, you must purchase a 32 | commercial license from the Licensor, its affiliated entities, or authorized 33 | resellers, or you must refrain from using the Licensed Work. 34 | 35 | All copies of the original and modified Licensed Work, and derivative works 36 | of the Licensed Work, are subject to this License. This License applies 37 | separately for each version of the Licensed Work and the Change Date may vary 38 | for each version of the Licensed Work released by Licensor. 39 | 40 | You must conspicuously display this License on each original or modified copy 41 | of the Licensed Work. If you receive the Licensed Work in original or 42 | modified form from a third party, the terms and conditions set forth in this 43 | License apply to your use of that work. 44 | 45 | Any use of the Licensed Work in violation of this License will automatically 46 | terminate your rights under this License for the current and all other 47 | versions of the Licensed Work. 48 | 49 | This License does not grant you any right in any trademark or logo of 50 | Licensor or its affiliates (provided that you may use a trademark or logo of 51 | Licensor as expressly required by this License). 52 | 53 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 54 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 55 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 56 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 57 | TITLE. 58 | 59 | MariaDB hereby grants you permission to use this License’s text to license 60 | your works, and to refer to it using the trademark "Business Source License", 61 | as long as you comply with the Covenants of Licensor below. 62 | 63 | ----------------------------------------------------------------------------- 64 | 65 | Covenants of Licensor 66 | 67 | In consideration of the right to use this License’s text and the "Business 68 | Source License" name and trademark, Licensor covenants to MariaDB, and to all 69 | other recipients of the licensed work to be provided by Licensor: 70 | 71 | 1. To specify as the Change License the GPL Version 2.0 or any later version, 72 | or a license that is compatible with GPL Version 2.0 or a later version, 73 | where "compatible" means that software provided under the Change License can 74 | be included in a program with software provided under GPL Version 2.0 or a 75 | later version. Licensor may specify additional Change Licenses without 76 | limitation. 77 | 78 | 2. To either: (a) specify an additional grant of rights to use that does not 79 | impose any additional restriction on the right granted in this License, as 80 | the Additional Use Grant; or (b) insert the text "None". 81 | 82 | 3. To specify a Change Date. 83 | 84 | 4. Not to modify this License in any other way. 85 | 86 | ----------------------------------------------------------------------------- 87 | 88 | Notice 89 | 90 | The Business Source License (this document, or the "License") is not an Open 91 | Source license. However, the Licensed Work will eventually be made available 92 | under an Open Source License, as stated in this License. 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![github](https://user-images.githubusercontent.com/111548547/192037827-733a5483-905a-4964-91d1-7e4cbf03fae9.png) 2 | 3 | [![Generic badge](https://img.shields.io/badge/ferum-docs-blue.svg)](https://ferum.gitbook.io/ferum-dex/) ![](https://img.shields.io/discord/1014040797487824896?label=discord) ![](https://img.shields.io/badge/liquidity-high-brightgreen) 4 | 5 | 6 | # Ferum 7 | 8 | Ferum is an on-chain order book offering unprecedented control to liquidity providers on [Aptos Ecosystem](https://twitter.com/AptosLabs)! 9 | For documentation on getting-started, better understanding of the architecture, and partnerships, please refer 10 | to [Ferum's Official Documentation](https://ferum.gitbook.io/ferum-dex/). 11 | 12 | Documentation below will mostly cover instructions for contributions & pull requests. 13 | 14 | ## Quick References 15 | 16 | 1. A good reference to the Move Language is the [Move Book](https://move-language.github.io/move/introduction.html). 17 | 2. The easiest way to publish a module is through the [Aptos CLI](https://aptos.dev/cli-tools/aptos-cli-tool/install-aptos-cli). 18 | 3. For developer support, join **🛠 #dev-discussions** group in [discord](http://discord.gg/ferum.). 19 | 4. A good read on [building super fast order books](https://gist.github.com/halfelf/db1ae032dc34278968f8bf31ee999a25?permalink_comment_id=3176518). 20 | 21 | ## Contributing 22 | 23 | We welcome all contributions; just make sure that you add unit tests to all new code added, and run `aptos move test` before making a pull request. 24 | 25 | ## Deployment Instructions 26 | 27 | All active development takes place on the main branch. During a release, all main branch commits get rebased on top of devnet branch. 28 | Once rebased, the ferum module either gets published under the same account if backwards compatible, or a new account if not. 29 | 30 | High level instructions for releasing a devnet branch: 31 | 32 | 1. Make sure all unit tests are passing! ✅ 33 | 1. Update your aptos CLI and make sure you have [latest](https://github.com/aptos-labs/aptos-core/releases/); usually breaks if you don't! Run `aptos --version` to find out which one you have and compare to the latest release. 34 | 2. `git checkout main; git pull --rebase` to get latest commits on the main branch. 35 | 3. `git checkout devnet; git rebase main devnet` to rebase all commits from main to devnet 36 | 4. Create a new profile via `ts-node cli/src/index.ts create-profile -n ferum-std-devnet` or use an existing account. 37 | 4. `aptos move publish --private-key 0xPRIVATE_KEY --max-gas 10000 --url https://fullnode.devnet.aptoslabs.com/v1 --included-artifacts none` to publish the module. 38 | 5. `git push` to synchronize remote branch once it's been properly published. 39 | -------------------------------------------------------------------------------- /benchmarking/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev="devnet" } 7 | 8 | [addresses] 9 | test = "0x20c74d8f316855963eafa8d4c7afd3bdc153b210df6834598a22284c0bc5f87a" 10 | -------------------------------------------------------------------------------- /benchmarking/sources/main.move: -------------------------------------------------------------------------------- 1 | module test::benchmarking { 2 | 3 | use std::vector; 4 | use std::signer::address_of; 5 | use aptos_std::table; 6 | 7 | struct Holder has key { 8 | t: table::Table, 9 | len: u64, 10 | } 11 | 12 | public entry fun create_vec(acc: &signer) { 13 | // Lets create the Holder with an empty vector. 14 | let t = table::new(); 15 | move_to(acc, Holder { 16 | t, 17 | len: 0, 18 | }) 19 | } 20 | 21 | public entry fun expand_vec(acc: &signer) acquires Holder { 22 | // Lets add to the vector to see what happens. 23 | let h = borrow_global_mut(address_of(acc)); 24 | let t = &mut h.t; 25 | let i = h.len; 26 | while (i < h.len + 1000) { 27 | table::add(t, i, 1); 28 | i = i + 1; 29 | }; 30 | h.len = i; 31 | } 32 | 33 | public entry fun add_to_vec(acc: &signer) acquires Holder { 34 | // Lets add to the vector to see what happens. 35 | let t = &mut borrow_global_mut(address_of(acc)).t; 36 | table::add(t, 900000, 1); 37 | } 38 | } -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /cli/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/Thumbs.db": true, 9 | "**/node_modules": true 10 | } 11 | } -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # Ferum CLI 2 | 3 | 1. Create a new profile: `ts-node src/index.ts create-profile -n ferum` (you can name it whatever). 4 | 5 | 1. Publish ferum modules: `ts-node src/index.ts publish-ferum -m ../contract`. 6 | 7 | 1. Create test coins: `ts-node src/index.ts create-test-coins`. 8 | 9 | 1. Initialize ferum: `ts-node src/index.ts init-ferum`. 10 | 11 | 1. Create a market: `ts-node src/index.ts init-market -ic FMA -qc FMB -id 3 -qd 3`. 12 | 13 | 1. Play with the commands below: 14 | 15 | ## Commands 16 | 17 | ### Add Limit Order 18 | 19 | ```terminal 20 | ts-node src/index.ts add-limit-order -ic FMA -qc FMB -p 2000 -q 1000 -s sell 21 | ``` 22 | 23 | ### Add Market Order 24 | 25 | ```terminal 26 | ts-node src/index.ts add-market-order -ic FMA -qc FMB -c 2000 -q 1000 -s sell 27 | ``` 28 | 29 | ### Cancel Order 30 | 31 | ```terminal 32 | ts-node lib/index.js cancel-order -ic FMA -qc FMB -id 12345 33 | ``` 34 | 35 | ### Create Test Coins 36 | 37 | ```terminal 38 | ts-node src/index.ts create-test-coin 39 | ``` 40 | 41 | ### Check Test Coin Balance 42 | 43 | ``` 44 | ts-node src/index.ts test-coin-balances 45 | ``` 46 | 47 | ### Init Market 48 | 49 | ```terminal 50 | ts-node src/index.ts init-market -ic FMA -qc FMB -id 3 -qd 3 51 | ``` 52 | 53 | ### Other Commands 54 | 55 | See `ts-node src/index.ts --help` for a full list of commands. 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /cli/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ferum-cli", 3 | "main": "lib/index.js", 4 | "bin": { 5 | "ferum-cli": "lib/index.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "jest", 10 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"", 11 | "lint": "tslint -p tsconfig.json" 12 | }, 13 | "keywords": [], 14 | "dependencies": { 15 | "@types/node": "^17.0.21", 16 | "aptos": "1.3.14", 17 | "commander": "^9.4.0", 18 | "js-sha3": "^0.8.0", 19 | "loglevel": "^1.8.0", 20 | "prettier": "^2.7.1", 21 | "util": "^0.12.4" 22 | }, 23 | "devDependencies": { 24 | "@types/jest": "^28.1.8", 25 | "jest": "^28.1.3", 26 | "ts-jest": "^28.0.8", 27 | "ts-node": "^10.9.1", 28 | "typescript": "^4.7.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cli/src/__tests__/config.test.ts: -------------------------------------------------------------------------------- 1 | import Config from "../config" 2 | 3 | describe("testing config", () => { 4 | test('should add address if necessary', () => { 5 | expect(Config._private.addAddressIfNecessary("0xc27207dd9813d91f069ebe109c269f17943e5b94271fef29e2292ab5e2f7706f", "test_coin::TestCoin")).toBe("0xc27207dd9813d91f069ebe109c269f17943e5b94271fef29e2292ab5e2f7706f::test_coin::TestCoin"); 6 | expect(Config._private.addAddressIfNecessary("0xc27207dd9813d91f069ebe109c269f17943e5b94271fef29e2292ab5e2f7706f", "0xc27207dd9813d91f069ebe109c269f17943e5b94271fef29e2292ab5e2f7706f::test_coin::TestCoin")).toBe("0xc27207dd9813d91f069ebe109c269f17943e5b94271fef29e2292ab5e2f7706f::test_coin::TestCoin"); 7 | }); 8 | }); 9 | 10 | -------------------------------------------------------------------------------- /cli/src/aptos-client.ts: -------------------------------------------------------------------------------- 1 | import { AptosClient, FaucetClient } from "aptos"; 2 | import Config from "./config"; 3 | import { assertUnreachable } from "./utils/types"; 4 | 5 | const DEVNET_NODE_URL = "https://fullnode.devnet.aptoslabs.com/v1"; 6 | const DEVNET_FAUCET_URL = "https://faucet.devnet.aptoslabs.com"; 7 | const TESTNET_NODE_URL = "https://fullnode.testnet.aptoslabs.com/v1"; 8 | const TESTNET_FAUCET_URL = "https://faucet.testnet.aptoslabs.com"; 9 | const LOCAL_NODE_URL = "http://localhost:8080/v1"; 10 | const LOCAL_FAUCET_URL = "http://localhost:8081"; 11 | 12 | const DevnetClient = new AptosClient(DEVNET_NODE_URL); 13 | const DevnetFaucetClient = new FaucetClient(DEVNET_NODE_URL, DEVNET_FAUCET_URL); 14 | 15 | const TestnetClient = new AptosClient(TESTNET_NODE_URL); 16 | const TestnetFaucetClient = new FaucetClient(TESTNET_NODE_URL, TESTNET_FAUCET_URL); 17 | 18 | const LocalClient = new AptosClient(LOCAL_NODE_URL); 19 | const LocalFaucetClient = new FaucetClient(LOCAL_NODE_URL, LOCAL_FAUCET_URL); 20 | 21 | export function getClient(): AptosClient { 22 | const env = Config.getEnv(); 23 | switch(env) { 24 | case 'devnet': 25 | return DevnetClient; 26 | case 'testnet': 27 | return TestnetClient; 28 | case 'local': 29 | return LocalClient; 30 | } 31 | assertUnreachable(env); 32 | } 33 | 34 | export function getFaucetClient(): FaucetClient { 35 | const env = Config.getEnv(); 36 | switch(env) { 37 | case 'devnet': 38 | return DevnetFaucetClient; 39 | case 'testnet': 40 | return TestnetFaucetClient; 41 | case 'local': 42 | return LocalFaucetClient; 43 | } 44 | assertUnreachable(env); 45 | } 46 | 47 | export function getNodeURL(): string { 48 | const env = Config.getEnv(); 49 | switch(env) { 50 | case 'devnet': 51 | return DEVNET_NODE_URL; 52 | case 'testnet': 53 | return TESTNET_NODE_URL; 54 | case 'local': 55 | return LOCAL_NODE_URL; 56 | } 57 | assertUnreachable(env); 58 | } 59 | 60 | -------------------------------------------------------------------------------- /cli/src/benchmarks/gas_benchmarks.ts: -------------------------------------------------------------------------------- 1 | import {AptosAccount, TxnBuilderTypes} from 'aptos'; 2 | import log from "loglevel"; 3 | 4 | import { getClient, getFaucetClient, getNodeURL } from '../aptos-client'; 5 | import { addOrder, initializeFerum, initializeMarket, addOrderTxnPayload } from '../market'; 6 | import {initializeUSDF, mintUSDF, registerUSDF} from '../usdf'; 7 | import { publishModuleUsingCLI } from "../utils/module-publish-utils"; 8 | import {sendSignedTransactionWithAccount, simulateTransactionWithAccount} from '../utils/transaction-utils'; 9 | import Config from '../config'; 10 | 11 | log.setLevel("info"); 12 | 13 | Config.setEnv('local'); 14 | 15 | const ORDER_COUNT = 10; 16 | 17 | async function main() { 18 | // First, create a new account to run the test on. 19 | const account = new AptosAccount(); 20 | log.info(`Creating account ${account.address().toString()} and funding with 1,000,000 APT`); 21 | 22 | let txHash = (await getFaucetClient().fundAccount(account.address(), 100000000000000))[0]; 23 | await getClient().waitForTransaction(txHash, {checkSuccess: true}); 24 | log.info(`Account is funded`); 25 | 26 | // Set account as local profile. 27 | Config.importExistingProfile("local", account.toPrivateKeyObject().privateKeyHex); 28 | 29 | // Deploy Ferum to that account. 30 | log.info('Publishing modules under account', account.address().toString()); 31 | await publishModuleUsingCLI(Config.getEnv(), "ferum", getNodeURL(), account, './contract', 2000000); 32 | log.info('Published'); 33 | 34 | // Create and mint USDF coin. 35 | log.info('Creating and minting USDF'); 36 | txHash = await initializeUSDF(account) 37 | await getClient().waitForTransaction(txHash, {checkSuccess: true}); 38 | txHash = await registerUSDF(account); 39 | await getClient().waitForTransaction(txHash, {checkSuccess: true}); 40 | txHash = await mintUSDF(account, 1000000000000); 41 | await getClient().waitForTransaction(txHash, {checkSuccess: true}); 42 | 43 | // Initialize Ferum. 44 | log.info('Initing Ferum'); 45 | txHash = await initializeFerum(account); 46 | await getClient().waitForTransaction(txHash, {checkSuccess: true}); 47 | 48 | // Initialize market. 49 | log.info('Initing Market'); 50 | txHash = await initializeMarket( 51 | account, 52 | '0x1::aptos_coin::AptosCoin', 4, 53 | `${account.address()}::test_coins::USDF`, 4, 54 | ); 55 | await getClient().waitForTransaction(txHash, {checkSuccess: true}); 56 | 57 | // Submit ORDER_COUNT sell and buy limit orders. 58 | log.info('Submitting Base Orders'); 59 | const promises = []; 60 | const midPrice = ORDER_COUNT * 10000; 61 | const {sequence_number: seqNum} = await getClient().getAccount(account.address()); 62 | for (let i = 0; i < ORDER_COUNT; i++) { 63 | promises.push(new Promise(async resolve => { 64 | setTimeout(async () => { 65 | let txHash = await addOrder( 66 | account, 67 | '0x1::aptos_coin::AptosCoin', 68 | `${account.address()}::test_coins::USDF`, 69 | 'sell', 70 | 'resting', 71 | midPrice + (i + 1), 72 | 1, 73 | {seqNum: Number(seqNum) + i, maxGas: 20000}, 74 | ); 75 | console.log(txHash); 76 | await getClient().waitForTransaction(txHash, {checkSuccess: true, timeoutSecs: 120}); 77 | resolve(); 78 | }, i * 100); 79 | })) 80 | } 81 | // for (let i = 0; i < ORDER_COUNT; i++) { 82 | // await addOrder( 83 | // account, 84 | // '0x1::aptos_coin::AptosCoin', 85 | // `${account.address()}::test_coins::USDF`, 86 | // 'buy', 87 | // 'resting', 88 | // midPrice - (i + 1), 89 | // 1, 90 | // ); 91 | // await getClient().waitForTransaction(txHash, {checkSuccess: true}); 92 | // } 93 | log.info('Waiting for resolution'); 94 | await Promise.all(promises); 95 | 96 | // Simulate order that will eat the entire sell side of a book. 97 | log.info('Simulating Order'); 98 | let payload = addOrderTxnPayload( 99 | account, 100 | '0x1::aptos_coin::AptosCoin', 101 | `${account.address()}::test_coins::USDF`, 102 | 'buy', 103 | 'resting', 104 | midPrice + 2*ORDER_COUNT, 105 | 1, 106 | ) 107 | await simulateTransactionWithAccount(account, payload, {maxGas: 200000}); 108 | } 109 | 110 | // main(); 111 | 112 | async function bench() { 113 | // First, create a new account to run the test on. 114 | const account = new AptosAccount(); 115 | log.info(`Creating account ${account.address().toString()} and funding with 1,000,000 APT`); 116 | 117 | let txHash = (await getFaucetClient().fundAccount(account.address(), 100000000000000))[0]; 118 | await getClient().waitForTransaction(txHash, {checkSuccess: true}); 119 | log.info(`Account is funded`); 120 | 121 | // Deploy benchmarking module to that account. 122 | log.info('Publishing modules under account', account.address().toString()); 123 | await publishModuleUsingCLI(Config.getEnv(), "test", getNodeURL(), account, './benchmarking', 10000); 124 | log.info('Published'); 125 | 126 | // Measure create vec. 127 | const createVec = TxnBuilderTypes.EntryFunction.natural( 128 | `${account.address()}::benchmarking`, 129 | "create_vec", 130 | [], 131 | [] 132 | ); 133 | await simulateTransactionWithAccount(account, createVec, {maxGas: 2000000}); 134 | // Actually create the vector. 135 | txHash = await sendSignedTransactionWithAccount(account, createVec, {maxGas: 2000000}) 136 | await getClient().waitForTransaction(txHash, {checkSuccess: true}); 137 | 138 | // Expand vec. 139 | const expand = TxnBuilderTypes.EntryFunction.natural( 140 | `${account.address()}::benchmarking`, 141 | "expand_vec", 142 | [], 143 | [] 144 | ); 145 | for (let i = 0; i < 10; i++) { 146 | txHash = await sendSignedTransactionWithAccount(account, expand, {maxGas: 2000000}) 147 | await getClient().waitForTransaction(txHash, {checkSuccess: true}); 148 | } 149 | 150 | // Simulate cost to add. 151 | const addTo = TxnBuilderTypes.EntryFunction.natural( 152 | `${account.address()}::benchmarking`, 153 | "add_to_vec", 154 | [], 155 | [] 156 | ); 157 | await simulateTransactionWithAccount(account, addTo, {maxGas: 2000000}) 158 | 159 | // 160 | // 161 | // txHash = await initializeFerum(account); 162 | // await getClient().waitForTransaction(txHash, {checkSuccess: true}); 163 | 164 | // simulateTransactionWithAccount(account, payload); 165 | } 166 | 167 | bench(); 168 | -------------------------------------------------------------------------------- /cli/src/config.ts: -------------------------------------------------------------------------------- 1 | import { AptosAccount, AptosAccountObject, } from "aptos"; 2 | import { assert } from "console"; 3 | import fs from "fs"; 4 | import log from "loglevel"; 5 | import {getClient, getFaucetClient} from "./aptos-client"; 6 | import { assertUnreachable } from "./utils/types"; 7 | 8 | type Profile = AptosAccountObject; 9 | 10 | export type Env = 'devnet' | 'testnet' | 'local'; 11 | 12 | type Config = { 13 | TypeAliases: { [key: string]: string }, 14 | Profiles: { [key: string]: Profile }, 15 | CurrentProfile: string | null, 16 | Env: Env, 17 | } 18 | 19 | export const CONFIG_PATH = `${process.env.HOME}/.ferum_config`; 20 | let ConfigCache: Config = { 21 | TypeAliases: {}, 22 | CurrentProfile: null, 23 | Profiles: {}, 24 | Env: 'testnet', 25 | }; 26 | 27 | if (!fs.existsSync(CONFIG_PATH)) { 28 | syncConfig() 29 | } else { 30 | ConfigCache = JSON.parse(fs.readFileSync(CONFIG_PATH).toString()); 31 | } 32 | 33 | function syncConfig() { 34 | fs.rmSync(CONFIG_PATH); 35 | fs.writeFileSync(CONFIG_PATH, JSON.stringify(ConfigCache, null, 2)); 36 | } 37 | 38 | function addAddressIfNecessary(address: string | null, type: string): string { 39 | if (!address) { 40 | return type; 41 | } 42 | if (type.split('::').length < 3) { 43 | return `${address}::${type}` 44 | } 45 | return type 46 | } 47 | 48 | export default { 49 | setEnv: function (env: Env) { 50 | ConfigCache.Env = env; 51 | syncConfig(); 52 | }, 53 | 54 | getEnv: function(): Env { 55 | return ConfigCache.Env; 56 | }, 57 | 58 | getFerumAddress: function (): string { 59 | let env = ConfigCache.Env; 60 | switch (env) { 61 | case 'devnet': 62 | case 'testnet': 63 | case 'local': 64 | return ConfigCache.Profiles[env].address; 65 | default: 66 | assertUnreachable(env); 67 | } 68 | }, 69 | 70 | getProfileAccount: function (name: string): AptosAccount { 71 | const profile = ConfigCache.Profiles[name]; 72 | if (!profile) throw new Error(`Profile ${name} not in Profile map.`); 73 | return AptosAccount.fromAptosAccountObject(profile); 74 | }, 75 | 76 | createNewProfile: async function (name: string) { 77 | const account = new AptosAccount(); 78 | if (name in ConfigCache.Profiles) { 79 | log.debug(`Overwriting profile ${name}`); 80 | } 81 | ConfigCache.Profiles[name] = account.toPrivateKeyObject(); 82 | syncConfig() 83 | }, 84 | 85 | importExistingProfile: function (name: string, privateKey: string) { 86 | const privateKeyHex = Uint8Array.from(Buffer.from(privateKey.replace("0x", ""), "hex")); 87 | const account = new AptosAccount(privateKeyHex) 88 | if (name in ConfigCache.Profiles) { 89 | log.debug(`Overwriting profile ${name}`); 90 | } 91 | ConfigCache.Profiles[name] = account.toPrivateKeyObject(); 92 | syncConfig() 93 | }, 94 | 95 | setCurrentProfile: function (name: string) { 96 | if (!(name in ConfigCache.Profiles)) { 97 | throw new Error(`${name} not a defined profile`); 98 | } 99 | ConfigCache.CurrentProfile = name; 100 | syncConfig() 101 | }, 102 | 103 | getCurrentProfileName: function (): string | null { 104 | return ConfigCache.CurrentProfile; 105 | }, 106 | 107 | tryResolveAlias: function (maybeAlias: string): string { 108 | assert(this.getFerumAddress(), "Ferum address not set!") 109 | if (maybeAlias in ConfigCache['TypeAliases']) { 110 | return addAddressIfNecessary( 111 | this.getFerumAddress(), 112 | ConfigCache['TypeAliases'][maybeAlias], 113 | ); 114 | } 115 | return maybeAlias; 116 | }, 117 | 118 | setAliasForType: function (alias: string, type: string) { 119 | if (alias in ConfigCache['TypeAliases']) { 120 | log.debug(`Overwriting alias registered with type ${type}`); 121 | } 122 | ConfigCache['TypeAliases'][alias] = type; 123 | syncConfig() 124 | }, 125 | 126 | clearAlias: function (symbol: string): boolean { 127 | if (!(symbol in ConfigCache['TypeAliases'])) { 128 | log.debug(`Symbol ${symbol} not registered`); 129 | return false; 130 | } 131 | delete ConfigCache['TypeAliases'][symbol]; 132 | syncConfig() 133 | return true; 134 | }, 135 | 136 | getTypeAliasMap: function (): { [key: string]: string } { 137 | return ConfigCache.TypeAliases; 138 | }, 139 | 140 | _private: { 141 | addAddressIfNecessary 142 | } 143 | } -------------------------------------------------------------------------------- /cli/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from "commander"; 4 | import log from "loglevel"; 5 | import util from "util"; 6 | import { createTestCoin, getTestCoinBalance, TestCoinSymbol, TEST_COINS, getBalance } from "./test-coins"; 7 | import { initializeFerum, initializeMarket, addOrder, cancelOrder } from "./market"; 8 | import { AptosAccount } from "aptos"; 9 | import { Types } from "aptos"; 10 | import { publishModuleUsingCLI } from "./utils/module-publish-utils"; 11 | import { getClient, getFaucetClient, getNodeURL } from './aptos-client'; 12 | import Config, { CONFIG_PATH } from './config'; 13 | import { initializeUSDF } from './usdf'; 14 | import { testModuleUsingCLI } from "./utils/module-testing-utils"; 15 | 16 | const DEFAULT_CONTRACT_DIR = "../contract" 17 | 18 | // Register test coin symbols with ferum account alias. 19 | for (let symbol in TEST_COINS) { 20 | Config.setAliasForType(symbol, TEST_COINS[symbol as TestCoinSymbol]); 21 | } 22 | 23 | log.setLevel("info"); 24 | 25 | program.version("1.1.0", undefined, "Output the version number."); 26 | 27 | program.option("-l, --log-level ", "Log level") 28 | .hook('preAction', (cmd, subCmd) => { 29 | const { logLevel } = cmd.opts(); 30 | setLogLevel(logLevel); 31 | }); 32 | 33 | program.command("create-profile") 34 | .description('Initializes a profile.') 35 | .requiredOption('-n, --name ', 'Name for profile') 36 | .action(async (_, cmd) => { 37 | const { name } = cmd.opts(); 38 | await Config.createNewProfile(name); 39 | Config.setCurrentProfile(name); 40 | log.info(`Created profile ${name} and selected it as the current one`); 41 | }); 42 | 43 | program.command("import-existing-profile") 44 | .description('Imports an existing profile.') 45 | .requiredOption('-n, --name ', 'Name for profile') 46 | .requiredOption("-pk, --private-key [string]", "Private key assoicated with the existing profile.") 47 | .action(async (_, cmd) => { 48 | const { name, privateKey } = cmd.opts(); 49 | await Config.importExistingProfile(name, privateKey); 50 | Config.setCurrentProfile(name); 51 | log.info(`Added profile ${name} and selected it as the current one`); 52 | }); 53 | 54 | program.command("set-current-profile") 55 | .description('Sets specified profile as the current.') 56 | .requiredOption('-n, --name ', 'Name of profile') 57 | .action(async (_, cmd) => { 58 | const { name } = cmd.opts(); 59 | try { 60 | Config.setCurrentProfile(name); 61 | } 62 | catch { 63 | log.info(`${name} is not a profile. Create it using create-profile`); 64 | return; 65 | } 66 | 67 | log.info('Current profile is now', name); 68 | }); 69 | 70 | program.command("show-current-profile") 71 | .description('Shows the current profile.') 72 | .action(async (_, cmd) => { 73 | const name = Config.getCurrentProfileName(); 74 | if (!name) { 75 | log.info('Current profile not set. Set it using set-current-profile'); 76 | return; 77 | } 78 | prettyPrint(`Current profile: ${name}`, Config.getProfileAccount(name).toPrivateKeyObject()); 79 | }); 80 | 81 | program.command("show-type-aliases") 82 | .description('Show the type aliases that are currently set.') 83 | .action(async (_, cmd) => { 84 | prettyPrint('Defined Aliases symbols', Config.getTypeAliasMap()); 85 | }); 86 | 87 | program.command("set-type-alias") 88 | .description('Set an alias for a type. The alias can then be used as arguments to any command instead of the fully qualified type.') 89 | .requiredOption( 90 | "-t, --type ", 91 | `Fully qualified type for (address::module::type). If address in the type is omitted, ` + 92 | `the public address for DefaultModulePrivateKey in ${CONFIG_PATH} is used.` 93 | ) 94 | .requiredOption("-a, --alias ", "Alias for the type.") 95 | .action(async (_, cmd) => { 96 | const { type, alias } = cmd.opts(); 97 | Config.setAliasForType(alias, type); 98 | log.info(`Set symbol for coin type ${type} to ${alias}.`); 99 | }); 100 | 101 | program.command("set-env") 102 | .description('Set the environment. Should be either devnet or testnet') 103 | .requiredOption( 104 | "-e, --env ", 105 | `Environment to set` 106 | ) 107 | .action(async (_, cmd) => { 108 | const { env } = cmd.opts(); 109 | if (env === 'devnet' || env === 'testnet') { 110 | Config.setEnv(env); 111 | log.info(`Env set to ${env}`); 112 | return; 113 | } else { 114 | throw new Error('Unsupported env. Valid choices are devnet or testnet') 115 | }; 116 | }); 117 | 118 | program.command("get-env") 119 | .description('Get the currently set environment') 120 | .action(async (_, cmd) => { 121 | console.log(`The current environment is set to ${Config.getEnv()}`); 122 | }); 123 | 124 | program.command("clear-type-alias") 125 | .description('Clear a type alias.') 126 | .requiredOption("-a, --alias ", "Name of type alias.") 127 | .action(async (_, cmd) => { 128 | const { alias } = cmd.opts(); 129 | if (Config.clearAlias(alias)) { 130 | log.info(`Cleared type alias ${alias}.`); 131 | } 132 | }); 133 | 134 | program.command("get-address") 135 | .option("-pk, --private-key [string]", "Private key of the account to get the address for.") 136 | .action(async (_, cmd) => { 137 | const { privateKey } = cmd.opts(); 138 | const privateKeyHex = Uint8Array.from(Buffer.from(privateKey, "hex")); 139 | const account = new AptosAccount(privateKeyHex); 140 | log.info(`Address: ${account.address().toString()}`); 141 | }); 142 | 143 | signedCmd("publish-ferum-current-profile") 144 | .description("Publishes ferum to the current profile") 145 | .requiredOption("-m, --module-path ", "Module path.", DEFAULT_CONTRACT_DIR) 146 | .option("-g, --max-gas [number]", "Max gas used for transaction. Optional. Defaults to 10000.", "10000") 147 | .action(async (_, cmd) => { 148 | const { account, modulePath, maxGas } = cmd.opts(); 149 | const maxGasNum = parseNumber(maxGas, 'max-gas'); 150 | log.info(`Publishing modules to environment ${Config.getEnv()} under account`, account.address().toString()); 151 | try { 152 | await publishModuleUsingCLI(Config.getEnv(), "ferum", getNodeURL(), account, modulePath, maxGasNum); 153 | } 154 | catch { 155 | console.error('Unable to publish module.'); 156 | } 157 | }); 158 | 159 | program.command("deploy-dev") 160 | .description("Deploys ferum to all dev/test environments") 161 | .requiredOption("-m, --module-path ", "Module path.", DEFAULT_CONTRACT_DIR) 162 | .option("-g, --max-gas [number]", "Max gas used for transaction. Optional. Defaults to 10000.", "10000") 163 | .action(async (_, cmd) => { 164 | const { modulePath, maxGas } = cmd.opts(); 165 | const maxGasNum = parseNumber(maxGas, 'max-gas'); 166 | 167 | let oldEnv = Config.getEnv(); 168 | 169 | let publish = async function() { 170 | let env = Config.getEnv(); 171 | let account = Config.getProfileAccount(env); 172 | log.info(`Publishing modules to environment ${Config.getEnv()} under account`, account.address().toString()); 173 | await publishModuleUsingCLI(Config.getEnv(), "ferum", getNodeURL(), account, modulePath, maxGasNum); 174 | }; 175 | 176 | try { 177 | Config.setEnv('testnet'); 178 | await publish(); 179 | } catch { 180 | console.error('Unable to publish module.'); 181 | } finally { 182 | Config.setEnv(oldEnv); 183 | } 184 | }); 185 | 186 | signedCmd("test-ferum") 187 | .requiredOption("-m, --module-path ", "Module path.", DEFAULT_CONTRACT_DIR) 188 | .action(async (_, cmd) => { 189 | const { account, modulePath } = cmd.opts(); 190 | log.info('Testing modules under account', account.address().toString()); 191 | try { 192 | await testModuleUsingCLI(Config.getEnv(), "ferum", getNodeURL(), account, modulePath); 193 | } 194 | catch { 195 | console.error('Unable to publish module.'); 196 | } 197 | }); 198 | 199 | signedCmd("create-test-coins") 200 | .description('Create FakeMoneyA (FMA) and FakeMoneyB (FMB) test coins.') 201 | .action(async (_, cmd) => { 202 | const { account } = cmd.opts(); 203 | await createTestCoin(account, "FMA"); 204 | await createTestCoin(account, "FMB"); 205 | }); 206 | 207 | signedCmd("create-usdf") 208 | .description('Creates USDF coins for testing.') 209 | .action(async (_, cmd) => { 210 | const { account } = cmd.opts(); 211 | const txHash = await initializeUSDF(account); 212 | log.info(`Started pending transaction: ${txHash}.`) 213 | const txResult = await getClient().waitForTransactionWithResult(txHash) as Types.UserTransaction; 214 | prettyPrint(transactionStatusMessage(txResult), txResult) 215 | }); 216 | 217 | signedCmd("apt-balance") 218 | .description('Get APT coin balance for the current profile.') 219 | .action(async (_, cmd) => { 220 | const { account } = cmd.opts(); 221 | let balance = await getBalance( 222 | account.address(), 223 | "0x1::aptos_coin::AptosCoin", 224 | ); 225 | prettyPrint("Coin balance:", balance); 226 | }); 227 | 228 | signedCmd("test-coin-balances") 229 | .description('Get FakeMoneyA (FMA) and FakeMoneyB (FMB) balances for the signing account.') 230 | .action(async (_, cmd) => { 231 | const { account } = cmd.opts(); 232 | const balances: { [key: string]: number } = {}; 233 | for (let coinSymbol in TEST_COINS) { 234 | balances[coinSymbol] = await getTestCoinBalance(account, coinSymbol as TestCoinSymbol); 235 | } 236 | prettyPrint("Coin Balances", balances); 237 | }); 238 | 239 | 240 | signedCmd("init-ferum") 241 | .action(async (_, cmd) => { 242 | const { account } = cmd.opts(); 243 | const txHash = await initializeFerum(account) 244 | log.info(`Started pending transaction: ${txHash}.`) 245 | const txResult = await getClient().waitForTransactionWithResult(txHash) as Types.UserTransaction; 246 | prettyPrint(transactionStatusMessage(txResult), txResult) 247 | }); 248 | 249 | signedCmd("init-market") 250 | .requiredOption( 251 | "-ic, --instrument-coin-type ", 252 | "Instrument CoinType. Must be a fully qualified type (address::module::CoinType) or an alias." 253 | ) 254 | .requiredOption( 255 | "-id, --instrument-decimals ", 256 | "Decimal places for the instrument coin type. Must be <= coin::decimals(InstrumentCointType)." + 257 | "The sum of the instrument and quote decimals must also be <= " + 258 | "min(coin::decimals(InstrumentCoinType), coin::decimals(QuoteCoinType))" 259 | ) 260 | .requiredOption( 261 | "-qc, --quote-coin-type ", 262 | "Quote CoinType. Must be a fully qualified type (address::module::CoinType) or an alias." 263 | ) 264 | .requiredOption( 265 | "-qd, --quote-decimals ", 266 | "Decimal places for the quote coin type. Must be <= coin::decimals(QuoteCoinType). " + 267 | "The sum of the instrument and quote decimals must also be <= " + 268 | "min(coin::decimals(InstrumentCoinType), coin::decimals(QuoteCoinType))" 269 | ) 270 | .action(async (_, cmd) => { 271 | const { account, quoteDecimals, instrumentDecimals } = cmd.opts(); 272 | let { quoteCoinType, instrumentCoinType } = cmd.opts(); 273 | 274 | quoteCoinType = Config.tryResolveAlias(quoteCoinType); 275 | instrumentCoinType = Config.tryResolveAlias(instrumentCoinType); 276 | 277 | const instrumentDecimalsNum = parseNumber(instrumentDecimals, 'instrument-decimals'); 278 | const quoteDecimalsNum = parseNumber(quoteDecimals, 'quote-decimals'); 279 | 280 | const txHash = await initializeMarket(account, instrumentCoinType, instrumentDecimalsNum, quoteCoinType, quoteDecimalsNum); 281 | log.info(`Started pending transaction: ${txHash}.`) 282 | const txResult = await getClient().waitForTransactionWithResult(txHash) as Types.UserTransaction; 283 | prettyPrint(transactionStatusMessage(txResult), txResult) 284 | }); 285 | 286 | signedCmd("add-order") 287 | .requiredOption( 288 | "-ic, --instrument-coin-type ", 289 | "Instrument CoinType. Must be a fully qualified type (address::module::CoinType) or an alias." 290 | ) 291 | .requiredOption( 292 | "-qc, --quote-coin-type ", 293 | "Quote CoinType. Must be a fully qualified type (address::module::CoinType) or an alias." 294 | ) 295 | .requiredOption( 296 | "-p, --price ", 297 | "Limit price for the order, in terms of coin::Coin.", 298 | ) 299 | .requiredOption( 300 | "-q, --quantity ", 301 | "Quantity for the order, in terms of coin::Coin", 302 | ) 303 | .requiredOption( 304 | "-s, --side ", 305 | "Side for the order, either buy or sell.", 306 | ) 307 | .requiredOption( 308 | "-t, --type ", 309 | "Type of the order, one of resting, fok, ioc, or post.", 310 | ) 311 | .action(async (_, cmd) => { 312 | const { account, price, quantity, side, type } = cmd.opts(); 313 | let { quoteCoinType, instrumentCoinType } = cmd.opts(); 314 | 315 | quoteCoinType = Config.tryResolveAlias(quoteCoinType); 316 | instrumentCoinType = Config.tryResolveAlias(instrumentCoinType); 317 | 318 | const txHash = await addOrder(account, instrumentCoinType, quoteCoinType, side, type, price, quantity) 319 | log.info(`Started pending transaction: ${txHash}.`) 320 | const txResult = await getClient().waitForTransactionWithResult(txHash) as Types.UserTransaction; 321 | prettyPrint(transactionStatusMessage(txResult), txResult) 322 | }); 323 | 324 | 325 | signedCmd("cancel-order") 326 | .requiredOption( 327 | "-ic, --instrument-coin-type ", 328 | "Instrument CoinType. Must be a fully qualified type (address::module::CoinType) or an alias." 329 | ) 330 | .requiredOption( 331 | "-qc, --quote-coin-type ", 332 | "Quote CoinType. Must be a fully qualified type (address::module::CoinType) or an alias." 333 | ) 334 | .requiredOption("-id, --order-id ", "Order id.") 335 | .action(async (_, cmd) => { 336 | const { account, orderID } = cmd.opts(); 337 | let { quoteCoinType, instrumentCoinType } = cmd.opts(); 338 | 339 | quoteCoinType = Config.tryResolveAlias(quoteCoinType); 340 | instrumentCoinType = Config.tryResolveAlias(instrumentCoinType); 341 | 342 | const txHash = await cancelOrder(account, instrumentCoinType, quoteCoinType, orderID) 343 | log.info(`Started pending transaction: ${txHash}.`) 344 | const txResult = await getClient().waitForTransactionWithResult(txHash) as Types.UserTransaction; 345 | prettyPrint(transactionStatusMessage(txResult), txResult) 346 | }); 347 | 348 | // 349 | // Helpers 350 | // 351 | 352 | function signedCmd(name: string) { 353 | return program.command(name) 354 | .option( 355 | "-pk, --private-key ", 356 | `Private key of account used to sign transaction. Will fallback to profile-name if not set.`, 357 | ) 358 | .option( 359 | "-pn, --profile ", 360 | `Name of profile to use to sign this transaction. Will fallback to current profile if not set.`, 361 | ) 362 | .hook('preAction', cmd => { 363 | const { privateKey, profileName } = cmd.opts(); 364 | let account; 365 | if (privateKey) { 366 | account = new AptosAccount(Uint8Array.from(Buffer.from(privateKey))); 367 | } 368 | else if (profileName) { 369 | account = Config.getProfileAccount(profileName); 370 | } 371 | else { 372 | try { 373 | const name = Config.getCurrentProfileName(); 374 | account = Config.getProfileAccount(name); 375 | } 376 | catch { 377 | throw new Error( 378 | 'No profile selected. Create a new one using create-profile or set an existing one using set-current-profile', 379 | ); 380 | } 381 | } 382 | cmd.setOptionValue('account', account); 383 | }); 384 | } 385 | 386 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 387 | function setLogLevel(value: any) { 388 | if (value === undefined || value === null) { 389 | return; 390 | } 391 | log.info("Setting the log level to:", value, '\n'); 392 | log.setLevel(value); 393 | } 394 | 395 | function prettyPrint(description: string, obj: any) { 396 | log.info(description); 397 | log.info(util.inspect(obj, { colors: true, depth: 6, compact: false })); 398 | } 399 | 400 | function transactionStatusMessage(txResult: Types.UserTransaction) { 401 | return txResult.success ? ( 402 | 'Transaction Succeded' 403 | ) : ( 404 | 'Transaction Failed' 405 | ); 406 | } 407 | 408 | function parseNumber(n: any, paramName: string): number { 409 | const out = Number(n); 410 | if (Number.isNaN(out)) { 411 | throw new Error(`Invalid number for ${paramName} param`); 412 | } 413 | return out; 414 | } 415 | 416 | program.parseAsync(process.argv); -------------------------------------------------------------------------------- /cli/src/market.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AptosAccount, 3 | TxnBuilderTypes, 4 | BCS, 5 | } from "aptos"; 6 | 7 | import {sendSignedTransactionWithAccount, TxOptions} from "./utils/transaction-utils"; 8 | 9 | function coinTypeTags(instrumentCoin: string, quoteCoin: string) { 10 | const instrumentCoinTypeTag = new TxnBuilderTypes.TypeTagStruct( 11 | TxnBuilderTypes.StructTag.fromString(instrumentCoin) 12 | ); 13 | 14 | const quoteCoinTypeTag = new TxnBuilderTypes.TypeTagStruct( 15 | TxnBuilderTypes.StructTag.fromString(quoteCoin) 16 | ); 17 | return [instrumentCoinTypeTag, quoteCoinTypeTag] 18 | } 19 | 20 | export async function initializeFerum( 21 | signerAccount: AptosAccount, 22 | ) { 23 | const entryFunction = TxnBuilderTypes.EntryFunction.natural( 24 | `${signerAccount.address()}::admin`, 25 | "init_ferum", 26 | [], 27 | [ 28 | BCS.bcsSerializeU128(0), 29 | BCS.bcsSerializeU128(0), 30 | BCS.bcsSerializeU128(0), 31 | BCS.bcsSerializeU128(0), 32 | ] 33 | ); 34 | return await sendSignedTransactionWithAccount(signerAccount, entryFunction, {maxGas: 20000}) 35 | } 36 | 37 | export async function initializeMarket( 38 | signerAccount: AptosAccount, 39 | instrumentCoin: string, 40 | instrumentDecimals: number, 41 | quoteCoin: string, 42 | quoteDecimals: number, 43 | ) { 44 | const entryFunction = TxnBuilderTypes.EntryFunction.natural( 45 | `${signerAccount.address()}::market`, 46 | "init_market_entry", 47 | coinTypeTags(instrumentCoin, quoteCoin), 48 | [ 49 | BCS.bcsSerializeU8(instrumentDecimals), 50 | BCS.bcsSerializeU8(quoteDecimals), 51 | ] 52 | ); 53 | return await sendSignedTransactionWithAccount(signerAccount, entryFunction) 54 | } 55 | 56 | export async function cancelOrder( 57 | signerAccount: AptosAccount, 58 | orderID: number, 59 | instrumentCoinType: string, 60 | quoteCoinType: string, 61 | ) { 62 | const entryFunction = TxnBuilderTypes.EntryFunction.natural( 63 | `${signerAccount.address()}::market`, 64 | "cancel_order_entry", 65 | coinTypeTags(instrumentCoinType, quoteCoinType), 66 | [ 67 | BCS.bcsSerializeU128(orderID), 68 | ] 69 | ); 70 | return await sendSignedTransactionWithAccount(signerAccount, entryFunction) 71 | } 72 | 73 | export function addOrderTxnPayload( 74 | signerAccount: AptosAccount, 75 | instrumentCoin: string, 76 | quoteCoin: string, 77 | side: 'buy' | 'sell', 78 | type: 'resting' | 'ioc' | 'fok' | 'post', 79 | price: number, 80 | quantity: number, 81 | ) { 82 | let typ = 0; 83 | if (type === 'resting') { 84 | typ = 1; 85 | } else if (type === 'post') { 86 | typ = 2; 87 | } else if (type === 'ioc') { 88 | typ = 3; 89 | } else if (type === 'fok') { 90 | typ = 4; 91 | } 92 | return TxnBuilderTypes.EntryFunction.natural( 93 | `${signerAccount.address()}::market`, 94 | "add_order_entry", 95 | coinTypeTags(instrumentCoin, quoteCoin), 96 | [ 97 | BCS.bcsSerializeU8(side === 'buy' ? 2 : 1), 98 | BCS.bcsSerializeU8(typ), 99 | BCS.bcsSerializeUint64(price), 100 | BCS.bcsSerializeUint64(quantity), 101 | BCS.bcsSerializeStr(""), 102 | ] 103 | ); 104 | } 105 | 106 | export async function addOrder( 107 | signerAccount: AptosAccount, 108 | instrumentCoin: string, 109 | quoteCoin: string, 110 | side: 'buy' | 'sell', 111 | type: 'resting' | 'ioc' | 'fok' | 'post', 112 | price: number, 113 | quantity: number, 114 | opts?: TxOptions, 115 | ) { 116 | const entryFn = addOrderTxnPayload(signerAccount, instrumentCoin, quoteCoin, side, type, price, quantity); 117 | return await sendSignedTransactionWithAccount(signerAccount, entryFn, opts); 118 | } 119 | -------------------------------------------------------------------------------- /cli/src/test-coins.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AptosAccount, 3 | TxnBuilderTypes, 4 | BCS, 5 | MaybeHexString, 6 | HexString, 7 | } from "aptos"; 8 | import { getClient } from "./aptos-client"; 9 | import Config, { CONFIG_PATH } from "./config"; 10 | import { sendSignedTransactionWithAccount } from "./utils/transaction-utils"; 11 | 12 | export type TestCoinSymbol = 'FMA' | 'FMB'; 13 | 14 | export const TEST_COINS: {[key in TestCoinSymbol]: string} = { 15 | 'FMA': 'test_coins::FakeMoneyA', 16 | 'FMB': 'test_coins::FakeMoneyB', 17 | }; 18 | 19 | // Register type aliases for TEST coins. 20 | for (let symbol in TEST_COINS) { 21 | Config.setAliasForType(symbol, TEST_COINS[symbol as TestCoinSymbol]); 22 | } 23 | 24 | /** Initializes the new coin */ 25 | async function initializeCoin( 26 | accountFrom: AptosAccount, 27 | coinName: string, 28 | coinSymbol: string, 29 | ): Promise { 30 | const token = new TxnBuilderTypes.TypeTagStruct( 31 | TxnBuilderTypes.StructTag.fromString(coinName), 32 | ); 33 | const entryFunctionPayload = TxnBuilderTypes.EntryFunction.natural( 34 | "0x1::managed_coin", 35 | "initialize", 36 | [token], 37 | [ 38 | BCS.bcsSerializeStr(coinName), 39 | BCS.bcsSerializeStr(coinSymbol), 40 | BCS.bcsSerializeU8(6), 41 | BCS.bcsSerializeBool(false), 42 | ], 43 | ); 44 | 45 | 46 | return await sendSignedTransactionWithAccount( 47 | accountFrom, 48 | entryFunctionPayload 49 | ); 50 | } 51 | 52 | /** Receiver needs to register the coin before they can receive it */ 53 | async function registerCoin( 54 | coinReceiver: AptosAccount, 55 | coinName: string 56 | ): Promise { 57 | const token = new TxnBuilderTypes.TypeTagStruct( 58 | TxnBuilderTypes.StructTag.fromString( 59 | `${coinName}` 60 | ) 61 | ); 62 | 63 | const entryFunctionPayload = TxnBuilderTypes.EntryFunction.natural( 64 | "0x1::managed_coin", 65 | "register", 66 | [token], 67 | [] 68 | ); 69 | 70 | return await sendSignedTransactionWithAccount( 71 | coinReceiver, 72 | entryFunctionPayload 73 | ); 74 | } 75 | 76 | /** Mints the newly created coin to a specified receiver address */ 77 | async function mintCoin( 78 | coinOwner: AptosAccount, 79 | receiverAddress: HexString, 80 | amount: number, 81 | coinName: string 82 | ): Promise { 83 | const token = new TxnBuilderTypes.TypeTagStruct( 84 | TxnBuilderTypes.StructTag.fromString( 85 | `${coinName}` 86 | ) 87 | ); 88 | 89 | const entryFunctionPayload = TxnBuilderTypes.EntryFunction.natural( 90 | "0x1::managed_coin", 91 | "mint", 92 | [token], 93 | [ 94 | BCS.bcsToBytes( 95 | TxnBuilderTypes.AccountAddress.fromHex(receiverAddress.hex()) 96 | ), 97 | BCS.bcsSerializeUint64(amount), 98 | ] 99 | ); 100 | 101 | return await sendSignedTransactionWithAccount( 102 | coinOwner, 103 | entryFunctionPayload 104 | ); 105 | } 106 | 107 | export async function getBalance( 108 | accountAddress: MaybeHexString, 109 | coinName: string 110 | ): Promise { 111 | try { 112 | const resource = await getClient().getAccountResource( 113 | accountAddress, 114 | `0x1::coin::CoinStore<${coinName}>` 115 | ); 116 | return parseInt((resource.data as any)["coin"]["value"]); 117 | } catch (_) { 118 | return 0; 119 | } 120 | } 121 | 122 | export async function getTestCoinBalance( 123 | coinMasterAccount: AptosAccount, 124 | coinSymbol: TestCoinSymbol, 125 | ): Promise { 126 | const coinName = Config.tryResolveAlias(coinSymbol); 127 | return await getBalance( 128 | coinMasterAccount.address(), 129 | coinName 130 | ); 131 | } 132 | 133 | // Sets up the test coin for the specified account. 134 | export async function createTestCoin( 135 | coinMasterAccount: AptosAccount, 136 | coinSymbol: TestCoinSymbol, 137 | ) { 138 | const coinName = Config.tryResolveAlias(coinSymbol); 139 | 140 | // 0. Show current balance. 141 | const currentBalance = await getBalance( 142 | coinMasterAccount.address(), 143 | coinName, 144 | ); 145 | console.log( 146 | `\nCurrent ${coinSymbol} balance for ${coinMasterAccount.address()}: ${currentBalance}\n` 147 | ); 148 | 149 | let txHash; 150 | 151 | // 1. Initialize coin. 152 | console.log(`Initializing ${coinSymbol} | ${coinName}...`); 153 | txHash = await initializeCoin( 154 | coinMasterAccount, 155 | coinName, 156 | coinSymbol, 157 | ); 158 | console.log(`Transaction Hash: ${txHash}`); 159 | await getClient().waitForTransaction(txHash); 160 | 161 | console.log(`\n---\n`); 162 | 163 | // 2. Register coin to receive it. 164 | console.log(`Registering ${coinSymbol}`); 165 | txHash = await registerCoin( 166 | coinMasterAccount, 167 | coinName 168 | ); 169 | console.log(`Transaction Hash: ${txHash}`); 170 | await getClient().waitForTransaction(txHash); 171 | 172 | console.log(`\n---\n`); 173 | 174 | // 3. Mint new coins. 175 | console.log(`Minting 100 ${coinSymbol}`); 176 | txHash = await mintCoin( 177 | coinMasterAccount, 178 | coinMasterAccount.address(), 179 | 100 * Math.pow(10, 6), 180 | coinName 181 | ); 182 | console.log(`Transaction Hash: ${txHash}`); 183 | await getClient().waitForTransaction(txHash); 184 | 185 | console.log(`\n---\n`); 186 | 187 | // 4. Check new balance 188 | console.log( 189 | `New ${coinSymbol} balance: ${await getBalance( 190 | coinMasterAccount.address(), 191 | coinName 192 | )}` 193 | ); 194 | 195 | console.log(`\n---\n`); 196 | } 197 | -------------------------------------------------------------------------------- /cli/src/usdf.ts: -------------------------------------------------------------------------------- 1 | import { AptosAccount, TxnBuilderTypes, BCS } from "aptos"; 2 | import Config from "./config"; 3 | import { sendSignedTransactionWithAccount } from "./utils/transaction-utils"; 4 | 5 | export async function initializeUSDF( 6 | signerAccount: AptosAccount, 7 | ): Promise { 8 | const entryFunctionPayload = TxnBuilderTypes.EntryFunction.natural( 9 | `${signerAccount.address()}::test_coins`, 10 | "create_usdf", 11 | [], 12 | [] 13 | ); 14 | 15 | return await sendSignedTransactionWithAccount( 16 | signerAccount, 17 | entryFunctionPayload 18 | ); 19 | } 20 | 21 | /** Receiver needs to register the coin before they can receive it */ 22 | export async function registerUSDF( 23 | coinReceiver: AptosAccount, 24 | ): Promise { 25 | const token = new TxnBuilderTypes.TypeTagStruct( 26 | TxnBuilderTypes.StructTag.fromString( 27 | `${Config.getFerumAddress()}::test_coins::USDF` 28 | ) 29 | ); 30 | const entryFunctionPayload = TxnBuilderTypes.EntryFunction.natural( 31 | "0x1::managed_coin", 32 | "register", 33 | [token], 34 | [] 35 | ); 36 | return await sendSignedTransactionWithAccount( 37 | coinReceiver, 38 | entryFunctionPayload 39 | ); 40 | } 41 | 42 | export async function mintUSDF( 43 | signer: AptosAccount, 44 | amount: number, 45 | ): Promise { 46 | const entryFunctionPayload = TxnBuilderTypes.EntryFunction.natural( 47 | `${Config.getFerumAddress()}::test_coins`, 48 | "mint_usdf", 49 | [], 50 | [ 51 | BCS.bcsSerializeUint64(amount), 52 | ] 53 | ); 54 | 55 | return await sendSignedTransactionWithAccount( 56 | signer, 57 | entryFunctionPayload 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /cli/src/utils/module-publish-utils.ts: -------------------------------------------------------------------------------- 1 | import { exec, execSync } from "child_process"; 2 | import { AptosAccount } from "aptos"; 3 | import { updateMoveTOMLForDeploy } from "./move-file-utils"; 4 | import { Env } from "../config"; 5 | 6 | /** Publishes a move module the aptos CLI under the hood */ 7 | export function publishModuleUsingCLI( 8 | env: Env, 9 | moduleName: string, 10 | rpcUrl: string, 11 | accountFrom: AptosAccount, 12 | moduleDir: string, 13 | maxGas: number, 14 | ): Promise { 15 | const pkeyHex = accountFrom.toPrivateKeyObject().privateKeyHex; 16 | const pkeyFlag = `--private-key ${pkeyHex}`; 17 | const maxGasFlag = `--max-gas ${maxGas}`; 18 | const dirFlag = `--package-dir ${moduleDir}`; 19 | const urlFlag = `--url ${rpcUrl}`; 20 | const addrFlag = `--named-addresses ${moduleName}=${accountFrom.address()}`; 21 | const artifactsFlag = `--included-artifacts none`; 22 | const gasUnitPriceFlag = `--gas-unit-price 200`; 23 | 24 | console.log(`aptos move publish --assume-yes ${gasUnitPriceFlag} ${maxGasFlag} ${pkeyFlag} ${dirFlag} ${urlFlag} ${addrFlag} ${artifactsFlag}`); 25 | 26 | const restoreMoveFile = updateMoveTOMLForDeploy(env, moduleName, moduleDir); 27 | 28 | return new Promise((resolve, reject) => { 29 | exec( 30 | `aptos move publish --assume-yes ${gasUnitPriceFlag} ${maxGasFlag} ${pkeyFlag} ${dirFlag} ${urlFlag} ${addrFlag} ${artifactsFlag}`, 31 | (err, stdout, stderr) => { 32 | if (stderr) { 33 | console.warn(stderr); 34 | } 35 | if (stdout) { 36 | console.warn(stdout); 37 | } 38 | 39 | restoreMoveFile(); 40 | 41 | if (err) { 42 | reject(err.code || 1); 43 | } 44 | else { 45 | resolve(0); 46 | } 47 | }, 48 | ); 49 | }); 50 | } -------------------------------------------------------------------------------- /cli/src/utils/module-testing-utils.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import { AptosAccount } from "aptos"; 3 | import { updateMoveTOMLForDeploy } from "./move-file-utils"; 4 | import { Env } from "../config"; 5 | 6 | /** Tests a move module the aptos CLI under the hood */ 7 | export function testModuleUsingCLI( 8 | env: Env, 9 | moduleName: string, 10 | rpcUrl: string, 11 | accountFrom: AptosAccount, 12 | moduleDir: string, 13 | ): Promise { 14 | const dirFlag = `--package-dir ${moduleDir}`; 15 | const addrFlag = `--named-addresses ${moduleName}=${accountFrom.address()}`; 16 | 17 | const restoreMoveFile = updateMoveTOMLForDeploy(env, moduleName, moduleDir); 18 | 19 | return new Promise((resolve, reject) => { 20 | exec( 21 | `aptos move test ${dirFlag} ${addrFlag}`, 22 | (err, stdout, stderr) => { 23 | if (stderr) { 24 | console.warn(stderr); 25 | } 26 | if (stdout) { 27 | console.warn(stdout); 28 | } 29 | 30 | restoreMoveFile(); 31 | 32 | if (err) { 33 | reject(err.code || 1); 34 | } else { 35 | resolve(0); 36 | } 37 | }, 38 | ); 39 | }); 40 | } -------------------------------------------------------------------------------- /cli/src/utils/move-file-utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { execSync } from "child_process"; 3 | import { Env } from "../config"; 4 | 5 | export type RestoreFn = () => void; 6 | 7 | export function updateMoveTOMLForDeploy(env: Env, moduleName: string, moduleDir: string): RestoreFn { 8 | const moveFilePath = path.join(moduleDir, 'Move.toml'); 9 | 10 | execSync(`cp ${moveFilePath} ${moveFilePath}.backup`); 11 | execSync(`sed -i "" '/${moduleName} *= */s/.*//' ${moveFilePath}`); 12 | if (env !== 'local') { 13 | // If publishing locally, don't rewrite any of the dependency revisions. 14 | execSync(`sed -i "" 's/rev *= *".*"/rev="${env}"/' ${moveFilePath}`); 15 | } 16 | 17 | return function() { 18 | execSync(`cp ${moveFilePath}.backup ${moveFilePath}`); 19 | execSync(`rm ${moveFilePath}.backup`); 20 | } 21 | } -------------------------------------------------------------------------------- /cli/src/utils/transaction-utils.ts: -------------------------------------------------------------------------------- 1 | import { AptosAccount, TxnBuilderTypes } from "aptos"; 2 | import { getClient } from '../aptos-client'; 3 | 4 | export type TxOptions = { 5 | maxGas?: number, 6 | seqNum?: string | number | null, 7 | }; 8 | 9 | const DefaultTxOpts: TxOptions = {}; 10 | 11 | export async function sendSignedTransactionWithPrivateKey( 12 | signerPrivateKey: string, 13 | entryFunction: TxnBuilderTypes.EntryFunction, 14 | opts: TxOptions = DefaultTxOpts, 15 | ) { 16 | const signerPrivateKeyHex = Uint8Array.from( 17 | Buffer.from(signerPrivateKey, "hex") 18 | ); 19 | const signerAccount = new AptosAccount(signerPrivateKeyHex); 20 | return await sendSignedTransactionWithAccount(signerAccount, entryFunction, opts) 21 | } 22 | 23 | export async function sendSignedTransactionWithAccount( 24 | signerAccount: AptosAccount, 25 | entryFunction: TxnBuilderTypes.EntryFunction, 26 | opts: TxOptions = DefaultTxOpts, 27 | ) { 28 | const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(entryFunction); 29 | 30 | let {maxGas = 2000, seqNum = null} = opts; 31 | 32 | // Ge the latest sequence number and chain id. 33 | let chain: number = 0; 34 | if (!seqNum) { 35 | const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([ 36 | getClient().getAccount(signerAccount.address()), 37 | getClient().getChainId(), 38 | ]); 39 | seqNum = sequenceNumber; 40 | chain = chainId; 41 | } else { 42 | chain = await getClient().getChainId(); 43 | } 44 | 45 | const rawTxn = new TxnBuilderTypes.RawTransaction( 46 | // Transaction sender account address 47 | TxnBuilderTypes.AccountAddress.fromHex(signerAccount.address()), 48 | BigInt(seqNum), 49 | entryFunctionPayload, 50 | // Max gas unit to spend 51 | BigInt(maxGas), 52 | // Gas price per unit 53 | BigInt(100), 54 | // Expiration timestamp. Transaction is discarded if it is not executed within 10 seconds from now. 55 | BigInt(Math.floor(Date.now() / 1000) + 10), 56 | new TxnBuilderTypes.ChainId(chain), 57 | ); 58 | const signedTxn = await getClient().signTransaction(signerAccount, rawTxn); 59 | const pendingTxn = await getClient().submitSignedBCSTransaction(signedTxn); 60 | return pendingTxn.hash; 61 | } 62 | 63 | export async function simulateTransactionWithAccount( 64 | signerAccount: AptosAccount, 65 | entryFunction: TxnBuilderTypes.EntryFunction, 66 | {maxGas, seqNum}: TxOptions = DefaultTxOpts, 67 | ) { 68 | const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(entryFunction); 69 | 70 | // Ge the latest sequence number and chain id. 71 | let chain: number = 0; 72 | if (!seqNum) { 73 | const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([ 74 | getClient().getAccount(signerAccount.address()), 75 | getClient().getChainId(), 76 | ]); 77 | seqNum = sequenceNumber; 78 | chain = chainId; 79 | } else { 80 | chain = await getClient().getChainId(); 81 | } 82 | 83 | const rawTxn = new TxnBuilderTypes.RawTransaction( 84 | // Transaction sender account address 85 | TxnBuilderTypes.AccountAddress.fromHex(signerAccount.address()), 86 | BigInt(seqNum), 87 | entryFunctionPayload, 88 | // Max gas unit to spend 89 | BigInt(maxGas), 90 | // Gas price per unit 91 | BigInt(100), 92 | // Expiration timestamp. Transaction is discarded if it is not executed within 10 seconds from now. 93 | BigInt(Math.floor(Date.now() / 1000) + 10), 94 | new TxnBuilderTypes.ChainId(chain), 95 | ); 96 | const simulatedTxn = await getClient().simulateTransaction(signerAccount, rawTxn); 97 | console.log(simulatedTxn); 98 | return simulatedTxn[0].hash; 99 | } -------------------------------------------------------------------------------- /cli/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export function assertUnreachable(x: never): never { 2 | throw new Error("Didn't expect to get here"); 3 | } -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2021", 5 | "dom" 6 | ], 7 | "types": [ 8 | "node", 9 | "jest" 10 | ], 11 | "module": "commonjs", 12 | "target": "es2021", 13 | "outDir": "./lib", 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "moduleResolution": "node", 17 | "noImplicitAny": true, 18 | "allowJs": true, 19 | "preserveConstEnums": true, 20 | "sourceMap": true, 21 | }, 22 | "include": ["src"], 23 | "exclude": ["node_modules", "**/__tests__/*"] 24 | } -------------------------------------------------------------------------------- /contract/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ferum" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "devnet" } 7 | 8 | [addresses] 9 | ferum="0x728891241aa0dc576e23fa368c168f657b1364eb909c8444f28bc7e4a4a2268d" 10 | -------------------------------------------------------------------------------- /contract/README.md: -------------------------------------------------------------------------------- 1 | # ferrum.xyz 2 | -------------------------------------------------------------------------------- /contract/output: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/contract/output -------------------------------------------------------------------------------- /contract/sources/platform.move: -------------------------------------------------------------------------------- 1 | module ferum::platform { 2 | use std::signer::address_of; 3 | 4 | friend ferum::market; 5 | 6 | // 7 | // Errors 8 | // 9 | 10 | const ERR_PROTOCOL_ALREADY_REGISTERED: u64 = 600; 11 | const ERR_INVALID_PROTOCOL_ADDRESS: u64 = 601; 12 | 13 | // Capability used to identify order placement through a Ferum protocol. 14 | struct ProtocolCapability has store { 15 | protocolAddress: address, // 0x0 is reserved as the sentinal value. 16 | } 17 | 18 | // Struct used to store information about a protocol. 19 | struct ProtocolInfo has key {} 20 | 21 | public fun register_protocol(owner: &signer): ProtocolCapability { 22 | let ownerAddr = address_of(owner); 23 | assert!(!exists(ownerAddr), ERR_PROTOCOL_ALREADY_REGISTERED); 24 | assert!(is_address_valid(ownerAddr), ERR_INVALID_PROTOCOL_ADDRESS); 25 | move_to(owner, ProtocolInfo{}); 26 | 27 | ProtocolCapability{ 28 | protocolAddress: ownerAddr, 29 | } 30 | } 31 | 32 | public(friend) fun is_address_valid(addr: address): bool { 33 | return addr != @0x0 34 | } 35 | 36 | public(friend) fun get_protocol_address(cap: &ProtocolCapability): address { 37 | return cap.protocolAddress 38 | } 39 | 40 | #[test_only] 41 | public fun drop_protocol_capability(cap: ProtocolCapability) { 42 | let ProtocolCapability {protocolAddress: _} = cap; 43 | } 44 | } -------------------------------------------------------------------------------- /contract/sources/test/coin_test_helpers.move: -------------------------------------------------------------------------------- 1 | module ferum::coin_test_helpers { 2 | #[test_only] 3 | use aptos_framework::coin; 4 | #[test_only] 5 | use std::string; 6 | #[test_only] 7 | use std::signer::address_of; 8 | 9 | #[test_only] 10 | struct FMA {} 11 | 12 | #[test_only] 13 | struct FMB {} 14 | 15 | #[test_only] 16 | struct FakeMoneyACapabilities has key { 17 | burn: coin::BurnCapability, 18 | freeze: coin::FreezeCapability, 19 | mint: coin::MintCapability, 20 | } 21 | 22 | #[test_only] 23 | struct FakeMoneyBCapabilities has key { 24 | burn: coin::BurnCapability, 25 | freeze: coin::FreezeCapability, 26 | mint: coin::MintCapability, 27 | } 28 | 29 | #[test_only] 30 | public fun deposit_fake_coins( 31 | owner: &signer, 32 | amt: u64, 33 | user: &signer, 34 | ) acquires FakeMoneyACapabilities, FakeMoneyBCapabilities { 35 | register_and_deposit_fma(owner, user, amt); 36 | register_and_deposit_fmb(owner, user, amt); 37 | } 38 | 39 | #[test_only] 40 | public fun create_fake_coins(owner: &signer, decimals: u8) { 41 | let ( 42 | burn, 43 | freeze, 44 | mint 45 | ) = coin::initialize( 46 | owner, 47 | string::utf8(b"Fake Money A"), 48 | string::utf8(b"FMA"), 49 | decimals, 50 | true 51 | ); 52 | move_to(owner, FakeMoneyACapabilities { burn, freeze, mint }); 53 | 54 | let ( 55 | burn, 56 | freeze, 57 | mint 58 | ) = coin::initialize( 59 | owner, 60 | string::utf8(b"Fake Money B"), 61 | string::utf8(b"FMB"), 62 | decimals, 63 | true 64 | ); 65 | move_to(owner, FakeMoneyBCapabilities { burn, freeze, mint }); 66 | } 67 | 68 | #[test_only] 69 | public fun register_and_deposit_fma(owner: &signer, user: &signer, amt: u64) acquires FakeMoneyACapabilities { 70 | coin::register(user); 71 | deposit_fma(owner, user, amt); 72 | } 73 | 74 | #[test_only] 75 | public fun register_and_deposit_fmb(owner: &signer, user: &signer, amt: u64) acquires FakeMoneyBCapabilities { 76 | coin::register(user); 77 | deposit_fmb(owner, user, amt); 78 | } 79 | 80 | #[test_only] 81 | public fun deposit_fma(owner: &signer, user: &signer, amt: u64) acquires FakeMoneyACapabilities { 82 | let cap = borrow_global(address_of(owner)); 83 | coin::deposit(address_of(user), coin::mint(amt, &cap.mint)); 84 | } 85 | 86 | #[test_only] 87 | public fun deposit_fmb(owner: &signer, user: &signer, amt: u64) acquires FakeMoneyBCapabilities { 88 | let cap = borrow_global(address_of(owner)); 89 | coin::deposit(address_of(user), coin::mint(amt, &cap.mint)); 90 | } 91 | } -------------------------------------------------------------------------------- /contract/sources/test/test_coins.move: -------------------------------------------------------------------------------- 1 | // Module defining the CoinType for ferum's test coins. 2 | module ferum::test_coins { 3 | use aptos_framework::coin; 4 | use aptos_framework::coin::{BurnCapability, MintCapability, FreezeCapability}; 5 | use std::string; 6 | use std::signer::address_of; 7 | use std::signer; 8 | 9 | // Errors. 10 | const ERR_NOT_ADMIN: u64 = 1; 11 | 12 | // Used internally for testing. 13 | struct FakeMoneyA {} 14 | struct FakeMoneyB {} 15 | 16 | // Used in documentation. 17 | struct USDF {} 18 | 19 | struct USDFCap has key { 20 | burn: BurnCapability, 21 | mint: MintCapability, 22 | freeze: FreezeCapability, 23 | } 24 | 25 | public entry fun create_usdf(owner: &signer) { 26 | assert!(signer::address_of(owner) == @ferum, ERR_NOT_ADMIN); 27 | let ( 28 | burn, 29 | freeze, 30 | mint 31 | ) = coin::initialize( 32 | owner, 33 | string::utf8(b"Ferum USD"), 34 | string::utf8(b"USDF"), 35 | 8, 36 | true 37 | ); 38 | move_to(owner, USDFCap { 39 | burn, 40 | freeze, 41 | mint, 42 | }); 43 | } 44 | 45 | public entry fun mint_usdf(dest: &signer, amt: u64) acquires USDFCap { 46 | let cap = borrow_global_mut(@ferum); 47 | let minted = coin::mint(amt, &cap.mint); 48 | coin::deposit(address_of(dest), minted); 49 | } 50 | } -------------------------------------------------------------------------------- /contract/sources/test/test_utils.move: -------------------------------------------------------------------------------- 1 | module ferum::test_utils { 2 | #[test_only] 3 | use std::vector; 4 | #[test_only] 5 | use aptos_std::table; 6 | #[test_only] 7 | use std::string; 8 | 9 | // 10 | // Constants. 11 | // 12 | 13 | const LEFT_BRACKET: u8 = 0x28; 14 | const RIGHT_BRACKET: u8 = 0x29; 15 | const LEFT_SQ_BRACKET: u8 = 0x5b; 16 | const RIGHT_SQ_BRACKET: u8 = 0x5d; 17 | const COLON: u8 = 0x3a; 18 | const SPACE: u8 = 0x20; 19 | const T: u8 = 0x74; 20 | const F: u8 = 0x66; 21 | 22 | const NINE: u8 = 0x39; 23 | const EIGHT: u8 = 0x38; 24 | const SEVEN: u8 = 0x37; 25 | const SIX: u8 = 0x36; 26 | const FIVE: u8 = 0x35; 27 | const FOUR: u8 = 0x34; 28 | const THREE: u8 = 0x33; 29 | const TWO: u8 = 0x32; 30 | const ONE: u8 = 0x31; 31 | const ZERO: u8 = 0x30; 32 | 33 | // 34 | // String utils. 35 | // 36 | 37 | #[test_only] 38 | public fun charToNum(s: u8): (u64, bool) { 39 | if (s == NINE) { 40 | return (9, true) 41 | }; 42 | if (s == EIGHT) { 43 | return (8, true) 44 | }; 45 | if (s == SEVEN) { 46 | return (7, true) 47 | }; 48 | if (s == SIX) { 49 | return (6, true) 50 | }; 51 | if (s == FIVE) { 52 | return (5, true) 53 | }; 54 | if (s == FOUR) { 55 | return (4, true) 56 | }; 57 | if (s == THREE) { 58 | return (3, true) 59 | }; 60 | if (s == TWO) { 61 | return (2, true) 62 | }; 63 | if (s == ONE) { 64 | return (1, true) 65 | }; 66 | if (s == ZERO) { 67 | return (0, true) 68 | }; 69 | return (0, false) 70 | } 71 | 72 | #[test_only] 73 | public fun u64_from_bytes(bytes: &vector): u64 { 74 | let i = 0; 75 | let num = 0u64; 76 | let size = vector::length(bytes); 77 | while (i < size) { 78 | num = num * 10; 79 | let (val, ok) = charToNum(*vector::borrow(bytes, i)); 80 | assert!(ok, 0); 81 | num = num + val; 82 | i = i + 1; 83 | }; 84 | num 85 | } 86 | 87 | #[test_only] 88 | public fun u16_from_bytes(bytes: &vector): u16 { 89 | (u64_from_bytes(bytes) as u16) 90 | } 91 | 92 | #[test_only] 93 | public fun u64_vector_from_str(str: &string::String): vector { 94 | let out = vector::empty(); 95 | let i = 0; 96 | let size = string::length(str); 97 | let bytes = string::bytes(str); 98 | let holder = vector::empty(); 99 | let opened = false; 100 | let closed = false; 101 | while (i < size) { 102 | let (_, ok) = charToNum(*vector::borrow(bytes, i)); 103 | if (!ok) { 104 | if (*vector::borrow(bytes, i) == LEFT_SQ_BRACKET) { 105 | opened = true; 106 | }; 107 | if (*vector::borrow(bytes, i) == RIGHT_SQ_BRACKET) { 108 | closed = true; 109 | }; 110 | if (vector::length(&holder) > 0) { 111 | vector::push_back(&mut out, u64_from_bytes(&holder)); 112 | holder = vector::empty(); 113 | }; 114 | i = i + 1; 115 | continue 116 | }; 117 | vector::push_back(&mut holder, *vector::borrow(bytes, i)); 118 | i = i + 1; 119 | }; 120 | assert!(opened && closed, 0); 121 | out 122 | } 123 | 124 | #[test_only] 125 | public fun u16_vector_from_str(str: &string::String): vector { 126 | convert_u64_list_to_u16(&u64_vector_from_str(str)) 127 | } 128 | 129 | #[test_only] 130 | public fun u64_vector_to_str(elems: &vector): string::String { 131 | let str = s(b"[ "); 132 | let i = 0; 133 | let size = vector::length(elems); 134 | while (i < size) { 135 | string::append(&mut str, u64_to_string(*vector::borrow(elems, i))); 136 | string::append(&mut str, s(b" ")); 137 | i = i + 1; 138 | }; 139 | string::append(&mut str, s(b"]")); 140 | str 141 | } 142 | 143 | #[test_only] 144 | public fun u16_vector_to_str(elems: &vector): string::String { 145 | u64_vector_to_str(&convert_u16_list_to_u64(elems)) 146 | } 147 | 148 | #[test_only] 149 | public fun u128_to_string(value: u128): string::String { 150 | if (value == 0) { 151 | return string::utf8(b"0") 152 | }; 153 | let buffer = vector::empty(); 154 | while (value != 0) { 155 | vector::push_back(&mut buffer, ((48 + value % 10) as u8)); 156 | value = value / 10; 157 | }; 158 | vector::reverse(&mut buffer); 159 | string::utf8(buffer) 160 | } 161 | 162 | #[test_only] 163 | public fun u64_to_string(value: u64): string::String { 164 | u128_to_string((value as u128)) 165 | } 166 | 167 | #[test_only] 168 | public fun u32_to_string(value: u32): string::String { 169 | u128_to_string((value as u128)) 170 | } 171 | 172 | #[test_only] 173 | public fun u16_to_string(value: u16): string::String { 174 | u128_to_string((value as u128)) 175 | } 176 | 177 | #[test_only] 178 | public fun bool_to_string(value: bool): string::String { 179 | if (value) { 180 | s(b"t") 181 | } else { 182 | s(b"f") 183 | } 184 | } 185 | 186 | #[test_only] 187 | public fun s(bytes: vector): string::String { 188 | string::utf8(bytes) 189 | } 190 | 191 | // 192 | // Vector utils. 193 | // 194 | 195 | #[test_only] 196 | 197 | public fun assert_vector_contains(vec: &vector, elem: &T) { 198 | let size = vector::length(vec); 199 | let i = 0; 200 | while (i < size) { 201 | if (vector::borrow(vec, i) == elem) { 202 | return 203 | }; 204 | i = i + 1; 205 | }; 206 | abort 0 207 | } 208 | 209 | // Generates a list of elements in sequential order. 210 | #[test_only] 211 | public fun gen_sequential_list(count: u64, offset: u64): vector { 212 | let elems = vector::empty(); 213 | let i = 0; 214 | while (i < count) { 215 | vector::push_back(&mut elems, i+1+offset); 216 | i = i + 1; 217 | }; 218 | elems 219 | } 220 | 221 | // Generates a list of random elements in the given range. 222 | #[test_only] 223 | public fun gen_random_list(count: u64, min: u64, max: u64): vector { 224 | assert!(min < max, 0); 225 | assert!(max - min > 5*count, 0); // Want a big enough search space. 226 | let elems = vector::empty(); 227 | let i = 0; 228 | let j = 0; 229 | let seed = 18446744073709551615u64; 230 | let seen = table::new(); 231 | while (j < count) { 232 | let i3 = (i+1) + (i+1) + (i+1); 233 | let val = (seed % i3 % (max - min)) + min; 234 | if (table::contains(&seen, val)) { 235 | i = i + 100; 236 | continue 237 | }; 238 | table::add(&mut seen, val, true); 239 | assert!(val >= min && val <= max, 0); 240 | vector::push_back(&mut elems, val); 241 | i = i + 1; 242 | j = j + 1; 243 | }; 244 | table::drop_unchecked(seen); 245 | elems 246 | } 247 | 248 | #[test_only] 249 | public fun convert_u64_list_to_u16(list: &vector): vector { 250 | let elems = vector::empty(); 251 | let i = 0; 252 | let size = vector::length(list); 253 | while (i < size) { 254 | vector::push_back(&mut elems, (*vector::borrow(list, i) as u16)); 255 | i = i + 1; 256 | }; 257 | elems 258 | } 259 | 260 | #[test_only] 261 | public fun convert_u16_list_to_u64(list: &vector): vector { 262 | let elems = vector::empty(); 263 | let i = 0; 264 | let size = vector::length(list); 265 | while (i < size) { 266 | vector::push_back(&mut elems, (*vector::borrow(list, i) as u64)); 267 | i = i + 1; 268 | }; 269 | elems 270 | } 271 | 272 | // Generates a list of `count` copies of the given element. 273 | #[test_only] 274 | public fun gen_list(count: u64, elem: T): vector { 275 | let elems = vector::empty(); 276 | let i = 0; 277 | while (i < count) { 278 | vector::push_back(&mut elems, elem); 279 | i = i + 1; 280 | }; 281 | elems 282 | } 283 | 284 | #[test_only] 285 | public fun join(strs: vector, sep: vector): string::String { 286 | let out = s(b""); 287 | while (!vector::is_empty(&strs)) { 288 | string::append(&mut out, vector::remove(&mut strs, 0)); 289 | if (vector::length(&strs) > 0) { 290 | string::append_utf8(&mut out, sep); 291 | }; 292 | }; 293 | out 294 | } 295 | 296 | #[test_only] 297 | public fun assert_vector_equal(output: &vector, expected: &vector) { 298 | let equal = { 299 | if (vector::length(output) != vector::length(expected)) { 300 | false 301 | } else { 302 | let equal = true; 303 | let i = 0; 304 | while (i < vector::length(output)) { 305 | equal = vector::borrow(output, i) == vector::borrow(expected, i); 306 | if (!equal) { 307 | break 308 | }; 309 | i = i + 1; 310 | }; 311 | equal 312 | } 313 | }; 314 | if (!equal) { 315 | std::debug::print(&s(b"Vectors not equal")); 316 | std::debug::print(&s(b"Expected:")); 317 | std::debug::print(expected); 318 | std::debug::print(&s(b"Actual:")); 319 | std::debug::print(output); 320 | abort 0 321 | } 322 | } 323 | 324 | // 325 | // Fixedpoint utils. 326 | // 327 | 328 | #[test_only] 329 | public fun pretty_print_fp(val: u64, decimals: u8): string::String { 330 | if (val == 0) { 331 | return s(b"0") 332 | }; 333 | let digits = vector[]; 334 | while (val != 0) { 335 | vector::push_back(&mut digits, val % 10); 336 | val = val / 10; 337 | }; 338 | vector::reverse(&mut digits); 339 | let firstNonZero = false; 340 | let count = 0; 341 | let out = s(b""); 342 | while (!vector::is_empty(&digits)) { 343 | let digit = vector::pop_back(&mut digits); 344 | count = count + 1; 345 | if (digit != 0) { 346 | firstNonZero = true; 347 | } else if (!firstNonZero && count <= decimals) { 348 | continue 349 | }; 350 | string::insert(&mut out, 0, u64_to_string(digit)); 351 | if (count == decimals) { 352 | string::insert(&mut out, 0, s(b".")); 353 | }; 354 | }; 355 | if (string::sub_string(&out, 0, 1) == s(b".")) { 356 | string::insert(&mut out, 0, s(b"0")); 357 | }; 358 | out 359 | } 360 | } -------------------------------------------------------------------------------- /contract/sources/token.move: -------------------------------------------------------------------------------- 1 | module ferum::token { 2 | use std::signer::address_of; 3 | use aptos_framework::coin; 4 | use std::string; 5 | 6 | const ERR_NOT_ALLOWED: u64 = 1; 7 | 8 | // CoinType for the Ferum token. 9 | struct Fe {} 10 | 11 | // Capabiity store for Fe token. 12 | struct FeCapabilities has key { 13 | burn: coin::BurnCapability, 14 | freeze: coin::FreezeCapability, 15 | mint: coin::MintCapability, 16 | } 17 | 18 | public entry fun init_fe(owner: &signer) { 19 | assert!(address_of(owner) == @ferum, ERR_NOT_ALLOWED); 20 | let (burn, freeze, mint) = coin::initialize(owner, string::utf8(b"Ferum Token"),string::utf8(b"Fe"), 8, false); 21 | move_to(owner, FeCapabilities { 22 | burn, 23 | freeze, 24 | mint, 25 | }) 26 | } 27 | } -------------------------------------------------------------------------------- /scripts/aptos-ascii: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | 3 | import ast 4 | import re 5 | import subprocess 6 | import sys 7 | import os 8 | 9 | # Use `which aptos` command to find out where aptos is located. 10 | # If you don't have it, use brew to install it. 11 | APTOS_PATH = os.getenv('APTOS_PATH') or '/usr/local/bin/aptos' 12 | 13 | ascii_pattern = r'(\[([0-9]+, )*[0-9]+\])' 14 | 15 | def convert_ascii(match_obj): 16 | if match_obj.group(1) is not None: 17 | ascii_list = ast.literal_eval(match_obj.group(1)) 18 | return ''.join(chr(i) for i in ascii_list) 19 | else: 20 | return "" 21 | 22 | def main(): 23 | if APTOS_PATH == '': 24 | raise Exception('Define path to Aptos cli') 25 | 26 | args = sys.argv[1:] 27 | process = subprocess.Popen([APTOS_PATH] + args + ['--bytecode-version', '6'], stdout=subprocess.PIPE) 28 | for line_encoded in iter(process.stdout.readline, b''): 29 | line = line_encoded.decode() 30 | if "[debug]" in line: 31 | print(re.sub(ascii_pattern, convert_ascii, line), end='') 32 | else: 33 | print(line, end='') 34 | 35 | if __name__ == '__main__': 36 | main() -------------------------------------------------------------------------------- /scripts/install-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | GIT_DIR=$(git rev-parse --git-dir) 4 | 5 | echo "Installing hooks..." 6 | # this command creates symlink to our pre-commit script 7 | ln -s ../../scripts/pre-commit.sh $GIT_DIR/hooks/pre-commit 8 | echo "Done"! 9 | -------------------------------------------------------------------------------- /scripts/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Running pre-commit hook" 4 | ./scripts/run-tests.sh 5 | 6 | # $? stores exit value of the last command 7 | if [ $? -ne 0 ]; then 8 | echo "Tests must pass before commit!" 9 | exit 1 10 | fi 11 | 12 | -------------------------------------------------------------------------------- /scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ts-node cli/src/index.ts test-ferum -m contract 4 | npm test --prefix cli -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 | # Ferum 2 | -------------------------------------------------------------------------------- /www/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export enum BorderRadiusType { 4 | SMALL = "rounded-sm", 5 | MEDIUM = "rounded-[0.625rem]", 6 | LARGE = "rounded-full", 7 | } 8 | 9 | const Button = ({ 10 | title, 11 | href, 12 | target, 13 | disabled, 14 | fullWidth = false, 15 | fullHeight = false, 16 | borderRadius = BorderRadiusType.LARGE, 17 | didTapButton, 18 | }: { 19 | title: string; 20 | href?: string; 21 | target?: string; 22 | disabled?: boolean; 23 | fullWidth?: boolean, 24 | fullHeight?: boolean, 25 | borderRadius?: BorderRadiusType, 26 | didTapButton?: () => void, 27 | }) => { 28 | const buttonStyle = ` 29 | ${fullWidth ? "w-full" : ""} ${fullHeight ? "h-full" : ""} ${borderRadius} px-8 py-2 text-xl inline-flex items-center justify-center`; 30 | function ButtonAnchor() { 31 | return ( 32 | 37 | {title} 38 | 39 | ); 40 | } 41 | if (disabled) { 42 | return ( 43 | 46 | {title} 47 | 48 | ) 49 | } 50 | return ( 51 | <> 52 | {href !== undefined ? ( 53 | 54 | {ButtonAnchor()} 55 | 56 | ) : ( 57 | 58 | )} 59 | 60 | ); 61 | }; 62 | 63 | export default Button; 64 | -------------------------------------------------------------------------------- /www/components/Container.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | import Navigation from "./Navigation"; 4 | 5 | 6 | type ContainerProps = { 7 | showsNavigation?: boolean, 8 | disableResponsive?: boolean 9 | disableMargins?: boolean, 10 | children: ReactNode[] | ReactNode 11 | } 12 | 13 | const Container = ({showsNavigation, disableResponsive, disableMargins, children}: ContainerProps) => { 14 | return ( 15 | // 1. There is a top level flex box with height 100%. 16 | // 2. The first child is an optional navigation bar with a set height and no shrink. 17 | // 3. The second child is a div with responsive horizontal margins set to grow fully. 18 | // 4. The second child's top is offset by navigation's height to allow centering on screen. 19 | // 5. Z index on the child that has navigation is increased to prevent the second child eating events. 20 |
21 | {showsNavigation && ( 22 |
23 | 24 |
25 | )} 26 |
38 | {children} 39 |
40 |
41 | ); 42 | }; 43 | 44 | Container.defaultProps = {}; 45 | 46 | export default Container; 47 | -------------------------------------------------------------------------------- /www/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { useState } from "react"; 4 | import Button from "./Button"; 5 | 6 | export const NavigationLinks = { 7 | twitter: "https://twitter.com/ferumxyz", 8 | typeform: "https://survey.typeform.com/to/xVtSzJUI", 9 | discord: "https://discord.gg/ferum", 10 | docs: "https://docs.ferum.xyz/", 11 | github: "https://github.com/ferumlabs/", 12 | }; 13 | 14 | const CompanyLogo = (props: any) => { 15 | return ( 16 |
17 | 24 | 28 | 32 | 40 | 41 |
42 | ); 43 | }; 44 | 45 | const Navigation = () => { 46 | const [showsMobileMenu, setShowsMobileMenu] = useState(false); 47 | 48 | function toggleMobileMenu() { 49 | setShowsMobileMenu(!showsMobileMenu); 50 | } 51 | 52 | return ( 53 | 143 | ); 144 | }; 145 | 146 | Navigation.defaultProps = {}; 147 | 148 | export default Navigation; 149 | -------------------------------------------------------------------------------- /www/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /www/next-seo.config.ts: -------------------------------------------------------------------------------- 1 | import { DefaultSeoProps } from "next-seo"; 2 | 3 | const APP_DESCRIPTION = "Ferum is the most developer friendly on-chain order book on Aptos!" 4 | 5 | const APP_DEFAULT_SEO: DefaultSeoProps = { 6 | title: "Ferum", 7 | titleTemplate: "%s", 8 | description: APP_DESCRIPTION, 9 | canonical: "https://www.ferum.xyz", 10 | additionalLinkTags: [ 11 | { 12 | rel: "icon", 13 | href: "/favicon.ico", 14 | }, 15 | { 16 | rel: "preload", 17 | href: "/fonts/euclid-circular-a-regular.woff2", 18 | as: "font", 19 | type: "font/woff2", 20 | crossOrigin: "anonymous", 21 | }, 22 | ], 23 | openGraph: { 24 | type: "website", 25 | locale: "en_US", 26 | url: "ferum.xyz", 27 | title: "ferum", 28 | description: APP_DESCRIPTION, 29 | images: [ 30 | { 31 | url: "https://www.ferum.xyz/assets/og-cover-photo.png", 32 | width: 2000, 33 | height: 1000, 34 | alt: "Ferum Cover Photo", 35 | type: "image/jpeg", 36 | secureUrl: "https://www.ferum.xyz/assets/og-cover-photo.png", 37 | }, 38 | ], 39 | site_name: "ferum.xyz", 40 | }, 41 | twitter: { 42 | handle: "@ferumxyz", 43 | site: "ferum.xyz", 44 | cardType: "summary_large_image", 45 | }, 46 | }; 47 | 48 | export default APP_DEFAULT_SEO; -------------------------------------------------------------------------------- /www/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const moduleExports = { 4 | reactStrictMode: true, 5 | webpack: (config, { isServer }) => { 6 | if (!isServer) { 7 | config.resolve.fallback.fs = false; 8 | } 9 | return config; 10 | }, 11 | }; 12 | 13 | module.exports = moduleExports; -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ferum.xyz", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "12.1.0", 13 | "next-plausible": "^3.6.2", 14 | "next-seo": "^5.1.0", 15 | "react": "17.0.2", 16 | "react-dom": "17.0.2" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "17.0.18", 20 | "@types/react": "17.0.39", 21 | "autoprefixer": "^10.4.2", 22 | "eslint": "8.9.0", 23 | "eslint-config-next": "12.1.0", 24 | "postcss": "^8.4.6", 25 | "tailwindcss": "^3.0.23", 26 | "typescript": "4.5.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /www/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import DEFAULT_APP_SEO from "../next-seo.config"; 3 | import { DefaultSeo } from "next-seo"; 4 | import type { AppProps } from "next/app"; 5 | import Head from "next/head"; 6 | import PlausibleProvider from 'next-plausible' 7 | 8 | function MyApp({ Component, pageProps }: AppProps) { 9 | return ( 10 | <> 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default MyApp; 26 | -------------------------------------------------------------------------------- /www/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Image from "next/image"; 3 | 4 | import { NavigationLinks } from "../components/Navigation"; 5 | 6 | const Home: NextPage = () => { 7 | return ( 8 |
9 | 10 |
11 | 12 |
13 | 24 |
25 | ); 26 | }; 27 | 28 | export default Home; 29 | -------------------------------------------------------------------------------- /www/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /www/public/alchemy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /www/public/assets/og-cover-photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/assets/og-cover-photo.png -------------------------------------------------------------------------------- /www/public/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /www/public/docs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/favicon.ico -------------------------------------------------------------------------------- /www/public/fonts/euclid-circular-a-bold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/fonts/euclid-circular-a-bold-italic.woff2 -------------------------------------------------------------------------------- /www/public/fonts/euclid-circular-a-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/fonts/euclid-circular-a-bold.woff2 -------------------------------------------------------------------------------- /www/public/fonts/euclid-circular-a-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/fonts/euclid-circular-a-italic.woff2 -------------------------------------------------------------------------------- /www/public/fonts/euclid-circular-a-light-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/fonts/euclid-circular-a-light-italic.woff2 -------------------------------------------------------------------------------- /www/public/fonts/euclid-circular-a-light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/fonts/euclid-circular-a-light.woff2 -------------------------------------------------------------------------------- /www/public/fonts/euclid-circular-a-medium-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/fonts/euclid-circular-a-medium-italic.woff2 -------------------------------------------------------------------------------- /www/public/fonts/euclid-circular-a-medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/fonts/euclid-circular-a-medium.woff2 -------------------------------------------------------------------------------- /www/public/fonts/euclid-circular-a-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/fonts/euclid-circular-a-regular.woff2 -------------------------------------------------------------------------------- /www/public/fonts/euclid-circular-a-semibold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/fonts/euclid-circular-a-semibold-italic.woff2 -------------------------------------------------------------------------------- /www/public/fonts/euclid-circular-a-semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/fonts/euclid-circular-a-semibold.woff2 -------------------------------------------------------------------------------- /www/public/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /www/public/sand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferumlabs/ferum/024f33d01b3caa79dc95549a6a05049c70e61fe5/www/public/sand.png -------------------------------------------------------------------------------- /www/public/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /www/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: "euclid-circular-a-regular"; 7 | src: url("/fonts/euclid-circular-a-regular.woff2"); 8 | font-style: normal; 9 | font-weight: normal; 10 | font-display: swap; 11 | } 12 | 13 | @supports(padding:max(0px)) { 14 | body, header, footer { 15 | padding-top: min(0vmin, env(safe-area-inset-top)); 16 | } 17 | } 18 | 19 | :root { 20 | --main-bg-color: #642C27; 21 | } 22 | 23 | html, 24 | body, 25 | #__next { 26 | background-color: var(--main-bg-color); 27 | width: 100%; 28 | height: 100%; 29 | } 30 | 31 | nav { 32 | background-color: var(--main-bg-color); 33 | } 34 | 35 | .nav-link { 36 | @apply text-xl; 37 | @apply opacity-80; 38 | @apply hover:opacity-40; 39 | } 40 | 41 | .nav-link-title { 42 | @apply text-2xl; 43 | } 44 | 45 | p, 46 | a, 47 | span, 48 | button { 49 | color: white; 50 | @apply text-xl 51 | } 52 | 53 | footer > p, a { 54 | @apply text-sm 55 | } 56 | 57 | .text-link { 58 | @apply underline; 59 | } 60 | -------------------------------------------------------------------------------- /www/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | module.exports = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx}", 6 | "./components/**/*.{js,ts,jsx,tsx}", 7 | ], 8 | theme: { 9 | extend: {}, 10 | fontFamily: { 11 | sans: ['euclid-circular-a-regular', ...defaultTheme.fontFamily.sans], 12 | }, 13 | }, 14 | plugins: [], 15 | mode: 'jit', 16 | } 17 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------