├── Move.toml ├── README.md ├── sources ├── lending.move └── simple_map.move └── typescript ├── .gitignore └── src └── cli2.ts /Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "LendingTutorial" 3 | version = "0.0.1" 4 | 5 | [dependencies] 6 | AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "795fffb779fc5e321e21d2b95b8f9946c9215af6" } 7 | AptosStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-stdlib/", rev = "795fffb779fc5e321e21d2b95b8f9946c9215af6" } 8 | 9 | [addresses] 10 | hippo_tutorial = "a61e1e86e9f596e483283727d2739ba24b919012720648c29380f9cd0a96c11a" 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `move-to-ts`: TypeScript dev framework for Move 2 | Automatically generate TypeScript SDK from your Move contract: 3 | 4 | ```typescript 5 | const {client, account} = ...; 6 | 7 | // Load auto-generated App 8 | const app = new App(client).hippo_tutorial.lend2; 9 | 10 | // load User and LendingProtocol struct from chain 11 | const user = await app.loadUser(account.address()); 12 | const protocol = await app.loadLendingProtocol(app.moduleAddress, false); 13 | 14 | // call user_get_limits to compute some info about user's state 15 | const [isUserHealthy, totalBorrowValue, totalDepositValue] = user.user_get_limits(protocol); 16 | console.log(isUserHealthy, totalBorrowValue, totalDepositValue); 17 | 18 | // make a withdrawal 19 | await app.withdraw(account, u64(1000000), [app.FakeBTC.getTag()]); 20 | ``` 21 | 22 | This guide includes a naive lending protocol implemented in Move. The above snippet demonstrates how you can use the 23 | auto-generated TypeScript SDK to: 24 | - Load onchain data (`User` and `LendingProtocol`) 25 | - Directly call into functions written in Move 26 | ([user_get_limits](https://github.com/hippospace/tutorial-lending/blob/e4fba83e8da5e281df16005f2fe0e81658b3e32b/sources/lending.move#L325) 27 | is a Move function that computes a `User`'s total deposit and borrow values to determine if the user is "healthy") 28 | - Send transactions (withdraw 1000000 units of FakeBTC) 29 | 30 | 31 | Since this tutorial is targeted at Move developers, we assume that you are already familiar with the Move language. 32 | If that is not the case, we recommend you go through [these](https://aptos.dev/) learning resources first. 33 | 34 | # Step-by-step guide 35 | 36 | Now, let's get straight to business. In this guide, we will use a very naive lending protocol 37 | ([github here](https://github.com/hippospace/tutorial-lending)) to demonstrate how to 38 | 1. Automatically generate TypeScript SDK 39 | 2. Use the generated `App` interface from your frontend/TypeScript application 40 | 3. Generate CLI utility to interact with our contract 41 | 4. Simulate arbitrary computation in Move, and fetch the execution result in TypeScript 42 | 5. Execute Move code within JavaScript environment 43 | 44 | 45 | ## Step 1: Install `move-to-ts` 46 | ```bash 47 | $ cargo install --git https://github.com/hippospace/move-to-ts 48 | ``` 49 | 50 | Do note that `move-to-ts` is a rapidly evolving project. If you install it through cargo, you may need to frequently 51 | reinstall to pick up the latest features. 52 | 53 | ## Step 2: Clone the move contract 54 | ```bash 55 | $ git clone https://github.com/hippospace/tutorial-lending 56 | ``` 57 | 58 | The contract cloned above is a toy lending protocol written in Move. It provides basic features such as lending pool 59 | creation, user deposit/withdraw/borrow/repay, and leaves out liquidation or interest rate logic for simplicity. You 60 | may examine the full contract [here](https://github.com/hippospace/tutorial-lending/blob/main/sources/lending.move). 61 | 62 | ## Step 3: Compile the demo contract 63 | ```bash 64 | $ cd tutorial-lending 65 | $ aptos move compile 66 | ``` 67 | For the commands above, you do need to have already installed the aptos CLI tool. We recommend the latest devnet 68 | build which can be installed with: 69 | ```bash 70 | $ cargo install --git https://github.com/aptos-labs/aptos-core.git aptos --branch devnet 71 | ``` 72 | 73 | ## Step 4: generate TypeScript SDK 74 | ```bash 75 | $ move-to-ts -c -n lending -o typescript 76 | ``` 77 | In the command above, 78 | - `-c` instructs the transpiler to generate related CLI utilities 79 | - `-n lending` instructs the transpiler to generate a `package.json`, where the package name is `lending` 80 | - `-o typescript` instructs the transpiler to output generated files into the typescript folder. If not specified, 81 | it will instead output generated files to the `build/typescript` directory. 82 | 83 | After executing the last `move-to-ts` command, our transpiler has already translated the naive lending protocol's 84 | Move code to TypeScript, and saved it under the `typescript` folder. We can have a look at the files generated: 85 | 86 | ```bash 87 | $ cd typescript 88 | $ ls -l 89 | 90 | -rw-rw-r-- 1 mana mana 814 Aug 9 15:10 package.json 91 | drwxrwxr-x 6 mana mana 4096 Aug 9 15:10 src 92 | -rw-rw-r-- 1 mana mana 382 Aug 9 15:10 tsconfig.json 93 | ``` 94 | We note that `package.json` is the generated package file, and `tsconfig.json` contains information needed for our 95 | typescript compiler later. If we look at the actual files generated under `src`: 96 | 97 | ```bash 98 | $ ls src -l 99 | 100 | drwxrwxr-x 2 mana mana 4096 Aug 9 15:10 aptos_framework 101 | drwxrwxr-x 2 mana mana 4096 Aug 9 15:10 aptos_std 102 | -rw-rw-r-- 1 mana mana 7460 Aug 9 15:10 cli.ts 103 | drwxrwxr-x 2 mana mana 4096 Aug 9 15:10 hippo_tutorial 104 | -rw-rw-r-- 1 mana mana 681 Aug 9 15:10 index.ts 105 | drwxrwxr-x 2 mana mana 4096 Aug 9 15:10 std 106 | ``` 107 | 108 | We see there is one folder for each of the packages that our project depends on (`aptos_framework`, `aptos_std` 109 | and `std`), and one folder for our own package `hippo_tutorial`. You may examine the content of these files to see 110 | how our Move code is translated to TypeScript. 111 | 112 | ## Step 5: Build the SDK 113 | 114 | ```bash 115 | $ yarn install 116 | $ yarn build 117 | ``` 118 | 119 | ## Using the generated `App` interface 120 | 121 | ```typescript 122 | import { App } from "path-to-generated-sdk"; 123 | 124 | async function appDemo() { 125 | const {client, account} = ...; 126 | 127 | // Load auto-generated App 128 | const app = new App(client).hippo_tutorial.lend2; 129 | 130 | // load User and LendingProtocol struct from chain 131 | const user = await app.loadUser(account.address()); 132 | const protocol = await app.loadLendingProtocol(app.moduleAddress, false); 133 | 134 | // call user_get_limits to compute some info about user's state 135 | const [isUserHealthy, totalBorrowValue, totalDepositValue] = user.user_get_limits(protocol); 136 | console.log(isUserHealthy, totalBorrowValue, totalDepositValue); 137 | 138 | // make a withdrawal 139 | await app.withdraw(account, u64(1000000), [app.FakeBTC.getTag()]); 140 | } 141 | ``` 142 | 143 | Using the `App` interface, you can: 144 | - Load on-chain state 145 | - `app.loadStructName(ownerAddress, loadFull=true)` 146 | - When `loadFull` is `true`, the loader will automatically load all `IterableTable` key-value pairs embedded in 147 | the `IterableTable`. 148 | - Note that structs that contain the `aptos_std::table::Table` struct cannot be loaded in full since there is no 149 | easy way to enumearte all keys. If a struct contains an `aptos_std::table::Table`, you need to set `loadFull` to 150 | `false` otherwise execution will throw an error. 151 | - Execute functions written in Move 152 | - Build TransactionPayload (needed by frontend wallets) 153 | - Send transactions directly (useful in CLI) 154 | 155 | Details for above incoming... 156 | 157 | ## Use the CLI utility to fire transactions 158 | ```bash 159 | $ yarn cli 160 | 161 | Usage: move-ts-cli [options] [command] 162 | 163 | Move TS CLI generated by move-to-ts 164 | 165 | Options: 166 | -c, --config path to your aptos config.yml (generated with "aptos 167 | init") 168 | -p, --profile aptos config profile to use (default: "default") 169 | -h, --help display help for command 170 | 171 | Commands: 172 | lend2:admin-add-pool Create a new lending pool (admin-only) 173 | lend2:admin-init Initialize protocol information (admin-only) 174 | lend2:admin-update-price Update price of a particular coin (admin-only) 175 | lend2:borrow Borrow from the CoinType pool. May fail if user exceeds 176 | borrow limit. 177 | lend2:create-fake-user1 178 | lend2:create-fake-user2 179 | lend2:create-fake-user3 180 | lend2:deposit Make a deposit into the CoinType pool. May create User if 181 | User does not already exist 182 | lend2:init-fake-pools 183 | lend2:price-drop 184 | lend2:repay Repay existing debt in the CoinType pool. May fail if user 185 | does not have such debt. 186 | lend2:withdraw Withdraw from the CoinType pool. May fail if user exceeds 187 | borrow limit, or if he does not have enough deposit 188 | lend2:query-get-all-users 189 | help [command] display help for command 190 | ``` 191 | 192 | The last command `yarn cli` invokes the auto-generated TypeScript CLI utility. You can see that every command starts 193 | with `lend2` (the module name), followed by a specific command name. All of them are generated from the `cmd` 194 | annotation in our Move code. 195 | 196 | For example, the `lend2:deposit` command is automatically generated from the code below: 197 | ```move 198 | #[cmd(desc=b"Make a deposit into the CoinType pool. May create User if User does not already exist")] 199 | public entry fun deposit( 200 | user: &signer, 201 | amount: u64 202 | ) acquires LendingPoolReserve, LendingProtocol, User { 203 | ... 204 | } 205 | ``` 206 | 207 | You can invoke the command above by: 208 | ```bash 209 | $ yarn cli -c .aptos/config.yaml lend2:deposit COIN_TYPE_TAG coin_amount 210 | ``` 211 | Where `.aptos/config.yaml` should contain your aptos account information (created via `aptos init`). You do need to 212 | make sure that the account inside has been funded using `aptos account create --account ADDRESS --use-faucet`. 213 | 214 | ## Simulate onchain computation 215 | 216 | We have already deployed the toy lending contract to devnet and created a few test users using our devnet test coins. 217 | Inside our Move contract, we have a function that loops over the list of all users, computing each user's borrow 218 | limit to see if they can be liquidated: 219 | 220 | ```Move 221 | #[query] 222 | public entry fun get_all_users(initiator: &signer) acquires LendingProtocol, User { 223 | let protocol = borrow_global(@hippo_tutorial); 224 | let i = 0; 225 | let len = vector::length(&protocol.users); 226 | let list = AllUserInfo { 227 | healthy_users: vector::empty(), 228 | unhealthy_users: vector::empty(), 229 | }; 230 | while (i < len) { 231 | let user_addr = vector::borrow(&protocol.users, i); 232 | let user = borrow_global(*user_addr); 233 | let (is_healthy, borrow_value, deposit_value) = user_get_limits(user, protocol); 234 | let user_info = UserInfo { 235 | address: *user_addr, 236 | deposits: get_values(&user.deposits), 237 | borrows: get_values(&user.borrows), 238 | borrow_value, 239 | deposit_value, 240 | is_healthy, 241 | }; 242 | // if user cannot be liquidated, we add them to the healthy list 243 | if (is_healthy) { 244 | vector::push_back(&mut list.healthy_users, user_info); 245 | } 246 | // if user can be liquidated, we add them to the unhealthy list 247 | else { 248 | vector::push_back(&mut list.unhealthy_users, user_info); 249 | }; 250 | 251 | i = i + 1; 252 | }; 253 | 254 | // write result out 255 | move_to(initiator, list) 256 | } 257 | ``` 258 | 259 | Now, above might seem a bit weird: in an ordinary lending protocol, there can be hundreds of thousands of users and 260 | we usually use indexer data and off-chain scripts to identify the set of users that are eligible for liquidation. 261 | Why would anyone want to write that code in the smart contract itself? It would be way too expensive to execute anyway! 262 | 263 | Well, the function above would be very expensive indeed, unless it is run in simulation mode. We have recently 264 | introduced the 265 | [transaction simulation feature](https://github.com/aptos-labs/aptos-core/commit/82570ca3973d7207fe797b42cdb4181c0d1aca1f) 266 | into the Aptos fullnode. Using the simulation feature, we are essentially able to execute the above Move function in 267 | a fullnode (which has access to realtime chain state), and fetch the result of the simulation using TypeScript. All 268 | of these can be done without any gas expenditure. 269 | 270 | Indeed, let's see this in action: 271 | ```bash 272 | $ yarn cli -c .aptos/config.yaml lend2:query-get-all-users 273 | 274 | Using address 0xa61e1e86e9f596e483283727d2739ba24b919012720648c29380f9cd0a96c11a 275 | { 276 | "healthy_users": [ 277 | { 278 | "address": "0x498d8926f16eb9ca90cab1b3a26aa6f97a080b3fcbe6e83ae150b7243a00fb68", 279 | "deposits": [ 280 | { "pool_id": "3", "deposit_amount": "10000000" }, 281 | { "pool_id": "2", "deposit_amount": "10000000" }, 282 | { "pool_id": "1", "deposit_amount": "10000000" }, 283 | { "pool_id": "0", "deposit_amount": "10000000" } 284 | ], 285 | "borrows": [], 286 | "borrow_value": "0", 287 | "deposit_value": "55020000000", 288 | "is_healthy": true 289 | }, 290 | { 291 | "address": "0x49c5e3ec5041062f02a352e4a2d03ce2bb820d94e8ca736b08a324f8dc634790", 292 | "deposits": [ 293 | { "pool_id": "1", "deposit_amount": "100" }, 294 | { "pool_id": "0", "deposit_amount": "100" } 295 | ], 296 | "borrows": [ 297 | { "pool_id": "2", "borrow_amount": "100" } 298 | ], 299 | "borrow_value": "100", 300 | "deposit_value": "550000", 301 | "is_healthy": true 302 | } 303 | ], 304 | "unhealthy_users": [ 305 | { 306 | "address": "0xf70ac33c984f8b7bead655ad239d246f1c0e3ca55fe0b8bfc119aa529c4630e8", 307 | "deposits": [ 308 | { "pool_id": "1", "deposit_amount": "100" }, 309 | { "pool_id": "0", "deposit_amount": "100" } 310 | ], 311 | "borrows": [ 312 | { "pool_id": "2", "borrow_amount": "800000" } 313 | ], 314 | "borrow_value": "800000", 315 | "deposit_value": "550000", 316 | "is_healthy": false 317 | } 318 | ] 319 | } 320 | ``` 321 | 322 | What you see here are specific information about 3 users (all we've got for now). Two of them are healthy users and 323 | one of them is unhealthy. All of these is obtained from a single command. 324 | 325 | When we invoke the `query-get-all-users` command, our CLI utility sends a simulation request to one of the devnet 326 | fullnodes, and requests for the execution of the `get_all_users` function in our contract. Once the simulated 327 | transaction is performed, the fullnode returns all the state changes back to our CLI utility, and our CLI utility 328 | identifies the exact value to be displayed automatically. 329 | 330 | What does this mean for Move developers? It means that a lot of tasks that you used to need to do manually in 331 | TypeScript can be performed using your Move contract instead. Common examples for these tasks include: 332 | - Compute user's total deposit and borrow 333 | - Compute user's borrow limit 334 | - Compute lending pool's interest rate 335 | - Compute protocol TVL 336 | - Give AMM quotes 337 | 338 | NOTE: you need to place the `#[query]` attribute on top of a `public entry` function to ask the transpiler to 339 | generate the simulation utility. The simulation utility looks for a `move_to` statement at the end of the `public 340 | entry` function and returns its value. 341 | 342 | ## Frontend execution 343 | 344 | If you need to perform business-logic computation in the frontend to provide greater responsiveness in your UI, 345 | `move-to-ts` has you covered as well. For example, in our toy lending contract, we have written this Move function 346 | to compute a user's total deposit and total borrow value: 347 | 348 | ```move 349 | public fun compute_borrow_deposit_value(user: &User, protocol: &LendingProtocol): (u64, u64) { 350 | let deposit_value = 0u64; 351 | let deposit_tail = iterable_table::tail_key(&user.deposits); 352 | while (option::is_some(&deposit_tail)) { 353 | let pool_id = *option::borrow(&deposit_tail); 354 | let (position, prev, _) = iterable_table::borrow_iter(&user.deposits, pool_id); 355 | let pool = vector::borrow(&protocol.pools, pool_id); 356 | let value = position.deposit_amount * pool.coin_price; 357 | deposit_value = deposit_value + value; 358 | deposit_tail = prev; 359 | }; 360 | 361 | let borrow_value = 0u64; 362 | let borrow_tail = iterable_table::tail_key(&user.borrows); 363 | while (option::is_some(&borrow_tail)) { 364 | let pool_id = *option::borrow(&borrow_tail); 365 | let (position, prev, _) = iterable_table::borrow_iter(&user.borrows, pool_id); 366 | let pool = vector::borrow(&protocol.pools, pool_id); 367 | let value = position.borrow_amount * pool.coin_price; 368 | borrow_value = borrow_value + value; 369 | borrow_tail = prev; 370 | }; 371 | 372 | (borrow_value, deposit_value) 373 | } 374 | ``` 375 | 376 | The Move code above is automatically translated to TypeScript as: 377 | ```typescript 378 | export function compute_borrow_deposit_value_ ( 379 | user: User, 380 | protocol: LendingProtocol, 381 | $c: AptosDataCache, 382 | ): [U64, U64] { 383 | let borrow_tail, borrow_value, deposit_tail, deposit_value, pool, pool__4, pool_id, pool_id__1, position, position__2, prev, prev__3, value, value__5; 384 | deposit_value = u64("0"); 385 | deposit_tail = Aptos_std.Iterable_table.tail_key_(user.deposits, $c, [AtomicTypeTag.U64, new StructTag(new HexString("0xa61e1e86e9f596e483283727d2739ba24b919012720648c29380f9cd0a96c11a"), "lend2", "DepositPosition", [])]); 386 | while (Std.Option.is_some_(deposit_tail, $c, [AtomicTypeTag.U64])) { 387 | { 388 | pool_id = $.copy(Std.Option.borrow_(deposit_tail, $c, [AtomicTypeTag.U64])); 389 | [position, prev, ] = Aptos_std.Iterable_table.borrow_iter_(user.deposits, $.copy(pool_id), $c, [AtomicTypeTag.U64, new StructTag(new HexString("0xa61e1e86e9f596e483283727d2739ba24b919012720648c29380f9cd0a96c11a"), "lend2", "DepositPosition", [])]); 390 | pool = Std.Vector.borrow_(protocol.pools, $.copy(pool_id), $c, [new StructTag(new HexString("0xa61e1e86e9f596e483283727d2739ba24b919012720648c29380f9cd0a96c11a"), "lend2", "LendingPool", [])]); 391 | value = ($.copy(position.deposit_amount)).mul($.copy(pool.coin_price)); 392 | deposit_value = ($.copy(deposit_value)).add($.copy(value)); 393 | deposit_tail = $.copy(prev); 394 | } 395 | 396 | } 397 | borrow_value = u64("0"); 398 | borrow_tail = Aptos_std.Iterable_table.tail_key_(user.borrows, $c, [AtomicTypeTag.U64, new StructTag(new HexString("0xa61e1e86e9f596e483283727d2739ba24b919012720648c29380f9cd0a96c11a"), "lend2", "BorrowPosition", [])]); 399 | while (Std.Option.is_some_(borrow_tail, $c, [AtomicTypeTag.U64])) { 400 | { 401 | pool_id__1 = $.copy(Std.Option.borrow_(borrow_tail, $c, [AtomicTypeTag.U64])); 402 | [position__2, prev__3, ] = Aptos_std.Iterable_table.borrow_iter_(user.borrows, $.copy(pool_id__1), $c, [AtomicTypeTag.U64, new StructTag(new HexString("0xa61e1e86e9f596e483283727d2739ba24b919012720648c29380f9cd0a96c11a"), "lend2", "BorrowPosition", [])]); 403 | pool__4 = Std.Vector.borrow_(protocol.pools, $.copy(pool_id__1), $c, [new StructTag(new HexString("0xa61e1e86e9f596e483283727d2739ba24b919012720648c29380f9cd0a96c11a"), "lend2", "LendingPool", [])]); 404 | value__5 = ($.copy(position__2.borrow_amount)).mul($.copy(pool__4.coin_price)); 405 | borrow_value = ($.copy(borrow_value)).add($.copy(value__5)); 406 | borrow_tail = $.copy(prev__3); 407 | } 408 | 409 | } 410 | return [$.copy(borrow_value), $.copy(deposit_value)]; 411 | } 412 | ``` 413 | 414 | And you can invoke it from your TypeScript frontend simply by: 415 | 1. first fetching the `User` and `LendingProtocol` resources 416 | 2. then invoke the `compute_borrow_deposit_value` method directly on the fetched `User` object 417 | 418 | ```typescript 419 | const user = await User.load(...); 420 | const protocol = await LendingProtocol.load(...); 421 | const [borrowValue, depositValue] = user.compute_borrow_deposit_value(protocol); 422 | ``` 423 | 424 | Do note that the `compute_borrow_deposit_value` function is included as a method under the TypeScript `User` class 425 | because we used the `#[method]` attribute to instruct our compiler to do so: 426 | 427 | ```move 428 | #[method(check_borrow_within_limit, compute_borrow_deposit_value, user_get_limits)] 429 | struct User has key, store { 430 | deposits: iterable_table::IterableTable, 431 | borrows: iterable_table::IterableTable, 432 | } 433 | ``` 434 | 435 | ## Transaction Builder 436 | 437 | Of course, how is a TS SDK complete without transaction builders? While our generated CLI utility can help you fire 438 | transactions directly from the commandline, in frontends what we really need is a convenient way to construct 439 | transaction payloads. The generated payload would then be signed and submitted by wallets. 440 | 441 | To facilitate this process, `move-to-ts` generates one `buildPayload_x` TypeScript function for each `public entry` 442 | Move function in your contract. For example, for the `deposit` function that we discussed in Step 6, the CLI utility 443 | uses the following code: 444 | 445 | ```typescript 446 | const payload = Hippo_tutorial.Lend2.buildPayload_deposit(amount_, [CoinType_]); 447 | await sendPayloadTx(client, account, payload); 448 | ``` 449 | 450 | You just need to adapt the last line to be used with your frontend's wallet or wallet-adapter — We're building a 451 | wallet adapter to streamline all of these! 452 | -------------------------------------------------------------------------------- /sources/lending.move: -------------------------------------------------------------------------------- 1 | module hippo_tutorial::lend2 { 2 | use hippo_tutorial::simple_map; 3 | use aptos_std::table; 4 | use aptos_std::type_info::{TypeInfo, type_of}; 5 | use std::vector; 6 | use std::signer::address_of; 7 | use aptos_framework::coin::{Self, Coin}; 8 | 9 | struct LendingPoolReserve has key { 10 | reserve: Coin, 11 | } 12 | 13 | struct LendingPool has store { 14 | pool_id: u64, 15 | coin_type: TypeInfo, 16 | coin_price: u64, 17 | total_deposit: u64, 18 | total_borrow: u64, 19 | } 20 | 21 | struct DepositPosition has copy, drop, store { 22 | pool_id: u64, 23 | deposit_amount: u64, 24 | } 25 | 26 | struct BorrowPosition has copy, drop, store { 27 | pool_id: u64, 28 | borrow_amount: u64, 29 | } 30 | 31 | #[method(check_borrow_within_limit, compute_borrow_deposit_value, user_get_limits)] 32 | struct User has key, store { 33 | deposits: simple_map::SimpleMap, 34 | borrows: simple_map::SimpleMap, 35 | } 36 | 37 | struct LendingProtocol has key, store { 38 | users: vector
, 39 | pools: vector, 40 | pool_index: table::Table, 41 | } 42 | 43 | #[cmd(desc=b"Initialize protocol information (admin-only)")] 44 | public entry fun admin_init(admin: &signer) { 45 | assert!(address_of(admin) == @hippo_tutorial, 1000); 46 | 47 | // initialize LendingProtocol 48 | move_to(admin, LendingProtocol { 49 | users: vector::empty
(), 50 | pools: vector::empty(), 51 | pool_index: table::new() 52 | }) 53 | } 54 | 55 | #[cmd(desc=b"Create a new lending pool (admin-only)")] 56 | public entry fun admin_add_pool(admin: &signer, initial_price: u64) acquires LendingProtocol { 57 | assert!(address_of(admin) == @hippo_tutorial, 1000); 58 | 59 | // make sure pool does not already exist 60 | assert!(!exists>(address_of(admin)), 1001); 61 | 62 | // make sure CoinType is valid 63 | assert!(coin::is_coin_initialized(), 1002); 64 | 65 | // fetch protocol info 66 | let protocol = borrow_global_mut(@hippo_tutorial); 67 | 68 | // create LendingPool 69 | let pool_id = vector::length(&protocol.pools); 70 | let coin_type = type_of(); 71 | let pool = LendingPool { 72 | pool_id, 73 | coin_type, 74 | coin_price: initial_price, 75 | total_deposit: 0, 76 | total_borrow: 0, 77 | }; 78 | 79 | // create LendingPoolReserve 80 | move_to>(admin, LendingPoolReserve { 81 | reserve: coin::zero(), 82 | }); 83 | 84 | vector::push_back(&mut protocol.pools, pool); 85 | table::add(&mut protocol.pool_index, type_of(), pool_id) 86 | } 87 | 88 | #[cmd(desc=b"Update price of a particular coin (admin-only)")] 89 | public entry fun admin_update_price(admin: &signer, price: u64) acquires LendingProtocol { 90 | assert!(address_of(admin) == @hippo_tutorial, 1000); 91 | let protocol = borrow_global_mut(@hippo_tutorial); 92 | let coin_type = type_of(); 93 | let pool_id = *table::borrow(&protocol.pool_index, coin_type); 94 | let pool = vector::borrow_mut(&mut protocol.pools, pool_id); 95 | pool.coin_price = price; 96 | } 97 | 98 | public fun ensure_user_exists(user: &signer, protocol: &mut LendingProtocol) { 99 | if (!exists(address_of(user))) { 100 | vector::push_back(&mut protocol.users, address_of(user)); 101 | move_to(user, User { 102 | deposits: simple_map::create(), 103 | borrows: simple_map::create(), 104 | }) 105 | } 106 | } 107 | 108 | #[cmd(desc=b"Make a deposit into the CoinType pool. May create User if User does not already exist")] 109 | public entry fun deposit( 110 | user: &signer, 111 | amount: u64 112 | ) acquires LendingPoolReserve, LendingProtocol, User { 113 | let protocol = borrow_global_mut(@hippo_tutorial); 114 | ensure_user_exists(user, protocol); 115 | let coin_type = type_of(); 116 | let pool_id = *table::borrow(&protocol.pool_index, coin_type); 117 | let pool = vector::borrow_mut(&mut protocol.pools, pool_id); 118 | let reserve = borrow_global_mut>(@hippo_tutorial); 119 | let user_assets = borrow_global_mut(address_of(user)); 120 | 121 | // withdraw from user wallet 122 | let coin = coin::withdraw(user, amount); 123 | 124 | // get LendingPoolReserve 125 | 126 | make_deposit(user_assets, pool, coin, reserve); 127 | 128 | // no validation needed 129 | } 130 | 131 | #[cmd(desc=b"Withdraw from the CoinType pool. May fail if user exceeds borrow limit, or if he does not have enough deposit")] 132 | public entry fun withdraw( 133 | user: &signer, 134 | amount: u64, 135 | ) acquires LendingPoolReserve, LendingProtocol, User { 136 | let protocol = borrow_global_mut(@hippo_tutorial); 137 | ensure_user_exists(user, protocol); 138 | let coin_type = type_of(); 139 | let pool_id = *table::borrow(&protocol.pool_index, coin_type); 140 | let pool = vector::borrow_mut(&mut protocol.pools, pool_id); 141 | let reserve = borrow_global_mut>(@hippo_tutorial); 142 | let user_assets = borrow_global_mut(address_of(user)); 143 | 144 | let coin = make_withdrawal(user_assets, pool, amount, reserve); 145 | if (!coin::is_account_registered(address_of(user))) { 146 | coin::register(user); 147 | }; 148 | coin::deposit(address_of(user), coin); 149 | 150 | assert!(check_borrow_within_limit(user_assets, protocol), 4000); 151 | } 152 | 153 | #[cmd(desc=b"Borrow from the CoinType pool. May fail if user exceeds borrow limit.")] 154 | public entry fun borrow( 155 | user: &signer, 156 | amount: u64, 157 | ) acquires LendingPoolReserve, LendingProtocol, User { 158 | let protocol = borrow_global_mut(@hippo_tutorial); 159 | ensure_user_exists(user, protocol); 160 | let coin_type = type_of(); 161 | let pool_id = *table::borrow(&protocol.pool_index, coin_type); 162 | let pool = vector::borrow_mut(&mut protocol.pools, pool_id); 163 | let reserve = borrow_global_mut>(@hippo_tutorial); 164 | let user_assets = borrow_global_mut(address_of(user)); 165 | 166 | let coin = make_borrow(user_assets, pool, amount, reserve); 167 | if (!coin::is_account_registered(address_of(user))) { 168 | coin::register(user); 169 | }; 170 | coin::deposit(address_of(user), coin); 171 | 172 | assert!(check_borrow_within_limit(user_assets, protocol), 4000); 173 | } 174 | 175 | #[cmd(desc=b"Repay existing debt in the CoinType pool. May fail if user does not have such debt.")] 176 | public entry fun repay( 177 | user: &signer, 178 | amount: u64 179 | ) acquires LendingPoolReserve, LendingProtocol, User { 180 | let protocol = borrow_global_mut(@hippo_tutorial); 181 | ensure_user_exists(user, protocol); 182 | let coin_type = type_of(); 183 | let pool_id = *table::borrow(&protocol.pool_index, coin_type); 184 | let pool = vector::borrow_mut(&mut protocol.pools, pool_id); 185 | let reserve = borrow_global_mut>(@hippo_tutorial); 186 | let user_assets = borrow_global_mut(address_of(user)); 187 | 188 | // withdraw from user wallet 189 | let coin = coin::withdraw(user, amount); 190 | 191 | // get LendingPoolReserve 192 | 193 | make_repayment(user_assets, pool, coin, reserve); 194 | 195 | // no validation needed 196 | } 197 | 198 | public fun make_deposit( 199 | user: &mut User, 200 | pool: &mut LendingPool, 201 | coin: Coin, 202 | reserve: &mut LendingPoolReserve 203 | ) { 204 | let amount = coin::value(&coin); 205 | coin::merge(&mut reserve.reserve, coin); 206 | assert!(pool.coin_type == type_of(), 2001); 207 | let pool_id = pool.pool_id; 208 | 209 | // update pool number 210 | pool.total_deposit = pool.total_deposit + amount; 211 | // update user number 212 | if (!simple_map::contains_key(&user.deposits, &pool_id)) { 213 | // create DepositPosition 214 | simple_map::add(&mut user.deposits, pool_id, DepositPosition { 215 | pool_id, 216 | deposit_amount: amount, 217 | }) 218 | } 219 | else { 220 | let position = simple_map::borrow_mut(&mut user.deposits, &pool_id); 221 | position.deposit_amount = position.deposit_amount + amount; 222 | } 223 | } 224 | 225 | public fun make_withdrawal( 226 | user: &mut User, 227 | pool: &mut LendingPool, 228 | amount: u64, 229 | reserve: &mut LendingPoolReserve 230 | ): Coin { 231 | let coin = coin::extract(&mut reserve.reserve, amount); 232 | assert!(pool.coin_type == type_of(), 2001); 233 | let pool_id = pool.pool_id; 234 | 235 | // update pool number 236 | pool.total_deposit = pool.total_deposit - amount; 237 | // update user number 238 | let position = simple_map::borrow_mut(&mut user.deposits, &pool_id); 239 | position.deposit_amount = position.deposit_amount - amount; 240 | 241 | coin 242 | } 243 | 244 | public fun make_borrow( 245 | user: &mut User, 246 | pool: &mut LendingPool, 247 | amount: u64, 248 | reserve: &mut LendingPoolReserve 249 | ): Coin { 250 | let coin = coin::extract(&mut reserve.reserve, amount); 251 | assert!(pool.coin_type == type_of(), 2001); 252 | let pool_id = pool.pool_id; 253 | 254 | // update pool number 255 | pool.total_borrow = pool.total_borrow + amount; 256 | // update user number 257 | if (!simple_map::contains_key(&user.borrows, &pool_id)) { 258 | // create DepositPosition 259 | simple_map::add(&mut user.borrows, pool_id, BorrowPosition { 260 | pool_id, 261 | borrow_amount: amount, 262 | }) 263 | } 264 | else { 265 | let position = simple_map::borrow_mut(&mut user.borrows, &pool_id); 266 | position.borrow_amount = position.borrow_amount + amount; 267 | }; 268 | 269 | coin 270 | } 271 | 272 | public fun make_repayment( 273 | user: &mut User, 274 | pool: &mut LendingPool, 275 | coin: Coin, 276 | reserve: &mut LendingPoolReserve 277 | ) { 278 | let amount = coin::value(&coin); 279 | coin::merge(&mut reserve.reserve, coin); 280 | assert!(pool.coin_type == type_of(), 2001); 281 | let pool_id = pool.pool_id; 282 | 283 | // update pool number 284 | pool.total_borrow = pool.total_borrow - amount; 285 | // update user number 286 | let position = simple_map::borrow_mut(&mut user.borrows, &pool_id); 287 | position.borrow_amount = position.borrow_amount - amount; 288 | } 289 | 290 | public fun compute_borrow_deposit_value(user: &User, protocol: &LendingProtocol): (u64, u64) { 291 | let deposit_value = 0u64; 292 | let index = 0; 293 | let length = simple_map::length(&user.deposits); 294 | while (index < length) { 295 | let deposit = simple_map::borrow_by_index(&user.deposits, index); 296 | let pool = vector::borrow(&protocol.pools, deposit.pool_id); 297 | let value = deposit.deposit_amount * pool.coin_price; 298 | deposit_value = deposit_value + value; 299 | index = index + 1; 300 | }; 301 | 302 | let borrow_value = 0u64; 303 | index = 0; 304 | length = simple_map::length(&user.borrows); 305 | while (index < length) { 306 | let borrow = simple_map::borrow_by_index(&user.borrows, index); 307 | let pool = vector::borrow(&protocol.pools, borrow.pool_id); 308 | let value = borrow.borrow_amount * pool.coin_price; 309 | borrow_value = borrow_value + value; 310 | index = index + 1; 311 | }; 312 | 313 | (borrow_value, deposit_value) 314 | } 315 | 316 | public fun check_borrow_within_limit(user: &User, protocol: &LendingProtocol): bool { 317 | let (borrow_value, deposit_value) = compute_borrow_deposit_value(user, protocol); 318 | // borrow_value / deposit_value < 90 / 100 319 | // borrow_value * 100 < deposit_value * 90 320 | borrow_value * 100 < deposit_value * 90 321 | } 322 | 323 | public fun user_get_limits(user: &User, protocol: &LendingProtocol): (bool, u64, u64) { 324 | let (borrow_value, deposit_value) = compute_borrow_deposit_value(user, protocol); 325 | // borrow_value / deposit_value < 90 / 100 326 | // borrow_value * 100 < deposit_value * 90 327 | (borrow_value * 100 < deposit_value * 90, borrow_value, deposit_value) 328 | } 329 | 330 | #[app] 331 | public fun global_get_user_limits(user: address): (bool, u64, u64) acquires User, LendingProtocol { 332 | let user = borrow_global(user); 333 | let protocol = borrow_global(@hippo_tutorial); 334 | user_get_limits(user, protocol) 335 | } 336 | 337 | struct UserInfo has key, store { 338 | address: address, 339 | deposits: vector, 340 | borrows: vector, 341 | borrow_value: u64, 342 | deposit_value: u64, 343 | is_healthy: bool, 344 | } 345 | 346 | struct AllUserInfo has key, store { 347 | healthy_users: vector, 348 | unhealthy_users: vector, 349 | } 350 | 351 | public fun get_values(table: &simple_map::SimpleMap): 352 | vector { 353 | let list = vector::empty(); 354 | 355 | let index = 0; 356 | let length = simple_map::length(table); 357 | while (index < length) { 358 | let value = simple_map::borrow_by_index(table, index); 359 | vector::push_back(&mut list, *value); 360 | index = index + 1; 361 | }; 362 | 363 | list 364 | } 365 | 366 | #[query] 367 | public entry fun get_all_users(initiator: &signer) acquires LendingProtocol, User { 368 | let protocol = borrow_global(@hippo_tutorial); 369 | let i = 0; 370 | let len = vector::length(&protocol.users); 371 | let list = AllUserInfo { 372 | healthy_users: vector::empty(), 373 | unhealthy_users: vector::empty(), 374 | }; 375 | while (i < len) { 376 | let user_addr = vector::borrow(&protocol.users, i); 377 | let user = borrow_global(*user_addr); 378 | let (is_healthy, borrow_value, deposit_value) = user_get_limits(user, protocol); 379 | let user_info = UserInfo { 380 | address: *user_addr, 381 | deposits: get_values(&user.deposits), 382 | borrows: get_values(&user.borrows), 383 | borrow_value, 384 | deposit_value, 385 | is_healthy, 386 | }; 387 | if (is_healthy) { 388 | vector::push_back(&mut list.healthy_users, user_info); 389 | } 390 | else { 391 | vector::push_back(&mut list.unhealthy_users, user_info); 392 | }; 393 | 394 | i = i + 1; 395 | }; 396 | 397 | // write result out 398 | move_to(initiator, list) 399 | } 400 | 401 | struct FakeBTC {} 402 | struct FakeETH {} 403 | struct FakeUSDC {} 404 | struct FakeUSDT {} 405 | 406 | struct FreeCoins has key { 407 | btc_coin: Coin, 408 | eth_coin: Coin, 409 | usdc_coin: Coin, 410 | usdt_coin: Coin, 411 | 412 | btc_cap: coin::MintCapability, 413 | eth_cap: coin::MintCapability, 414 | usdc_cap: coin::MintCapability, 415 | usdt_cap: coin::MintCapability, 416 | 417 | btc_burn: coin::BurnCapability, 418 | eth_burn: coin::BurnCapability, 419 | usdc_burn: coin::BurnCapability, 420 | usdt_burn: coin::BurnCapability, 421 | 422 | btc_freeze: coin::FreezeCapability, 423 | eth_freeze: coin::FreezeCapability, 424 | usdc_freeze: coin::FreezeCapability, 425 | usdt_freeze: coin::FreezeCapability, 426 | } 427 | 428 | #[cmd] 429 | public entry fun init_fake_pools(admin: &signer) acquires LendingProtocol { 430 | use std::string; 431 | let name = string::utf8(b"name"); 432 | let (btc_burn, btc_freeze, btc_cap) = coin::initialize(admin, copy name, copy name, 0, false); 433 | let (eth_burn, eth_freeze, eth_cap) = coin::initialize(admin, copy name, copy name, 0, false); 434 | let (usdc_burn, usdc_freeze, usdc_cap) = coin::initialize(admin, copy name, copy name, 0, false); 435 | let (usdt_burn, usdt_freeze, usdt_cap) = coin::initialize(admin, copy name, copy name, 0, false); 436 | 437 | let mint_amount = 1000000000000; 438 | move_to(admin, FreeCoins { 439 | btc_coin: coin::mint(mint_amount, &btc_cap), 440 | eth_coin: coin::mint(mint_amount, ð_cap), 441 | usdc_coin: coin::mint(mint_amount, &usdc_cap), 442 | usdt_coin: coin::mint(mint_amount, &usdt_cap), 443 | 444 | btc_cap, 445 | eth_cap, 446 | usdc_cap, 447 | usdt_cap, 448 | 449 | btc_burn, 450 | eth_burn, 451 | usdc_burn, 452 | usdt_burn, 453 | 454 | btc_freeze, 455 | eth_freeze, 456 | usdc_freeze, 457 | usdt_freeze, 458 | }); 459 | 460 | admin_init(admin); 461 | admin_add_pool(admin, 10000); 462 | admin_add_pool(admin, 1000); 463 | admin_add_pool(admin, 1); 464 | admin_add_pool(admin, 1); 465 | } 466 | 467 | #[cmd] 468 | public entry fun price_drop(admin: &signer) acquires LendingProtocol { 469 | admin_update_price(admin, 5000); 470 | admin_update_price(admin, 500); 471 | } 472 | 473 | fun init_coin_stores(user: &signer) acquires FreeCoins { 474 | coin::register(user); 475 | coin::register(user); 476 | coin::register(user); 477 | coin::register(user); 478 | let faucet_amount = 1000000000; 479 | let free_coins = borrow_global_mut(@hippo_tutorial); 480 | let btc = coin::extract(&mut free_coins.btc_coin, faucet_amount); 481 | let eth = coin::extract(&mut free_coins.eth_coin, faucet_amount); 482 | let usdc = coin::extract(&mut free_coins.usdc_coin, faucet_amount); 483 | let usdt = coin::extract(&mut free_coins.usdt_coin, faucet_amount); 484 | let addr = address_of(user); 485 | coin::deposit(addr, btc); 486 | coin::deposit(addr, eth); 487 | coin::deposit(addr, usdc); 488 | coin::deposit(addr, usdt); 489 | } 490 | 491 | #[cmd] 492 | public entry fun create_fake_user1(user: &signer) acquires FreeCoins, LendingPoolReserve, LendingProtocol, User { 493 | init_coin_stores(user); 494 | // deposit a lot, borrow nothing 495 | deposit(user, 10000000); 496 | deposit(user, 10000000); 497 | deposit(user, 10000000); 498 | deposit(user, 10000000); 499 | } 500 | 501 | #[cmd] 502 | public entry fun create_fake_user2(user: &signer) acquires FreeCoins, LendingPoolReserve, LendingProtocol, User { 503 | init_coin_stores(user); 504 | // deposit a lot, borrow little 505 | deposit(user, 100); 506 | deposit(user, 100); 507 | borrow(user, 100); 508 | } 509 | 510 | #[cmd] 511 | public entry fun create_fake_user3(user: &signer) acquires FreeCoins, LendingPoolReserve, LendingProtocol, User { 512 | init_coin_stores(user); 513 | // deposit a lot, borrow a lot 514 | deposit(user, 100); 515 | deposit(user, 100); 516 | borrow(user, 800000); 517 | } 518 | 519 | #[test_only] 520 | fun test_init(admin: &signer, user1: &signer, user2: &signer, user3: &signer) acquires FreeCoins, LendingPoolReserve, LendingProtocol, User { 521 | use aptos_framework::account; 522 | account::create_account_for_test(address_of(admin)); 523 | account::create_account_for_test(address_of(user1)); 524 | account::create_account_for_test(address_of(user2)); 525 | account::create_account_for_test(address_of(user3)); 526 | init_fake_pools(admin); 527 | create_fake_user1(user1); 528 | create_fake_user2(user2); 529 | create_fake_user3(user3); 530 | } 531 | 532 | #[test(admin=@hippo_tutorial, user1=@0x1001, user2=@0x1002, user3=@0x1003)] 533 | public entry fun test_user_creation(admin: &signer, user1: &signer, user2: &signer, user3: &signer) acquires FreeCoins, LendingPoolReserve, LendingProtocol, User { 534 | test_init(admin, user1, user2, user3); 535 | } 536 | 537 | #[test(admin=@hippo_tutorial, user1=@0x1001, user2=@0x1002, user3=@0x1003)] 538 | public entry fun test_borrow_success(admin: &signer, user1: &signer, user2: &signer, user3: &signer) acquires FreeCoins, LendingPoolReserve, LendingProtocol, User { 539 | test_init(admin, user1, user2, user3); 540 | // user2 tries to borrow a lot should succeed 541 | borrow(user2, 800000); 542 | } 543 | 544 | #[expected_failure] 545 | #[test(admin=@hippo_tutorial, user1=@0x1001, user2=@0x1002, user3=@0x1003)] 546 | public entry fun test_borrow_failure(admin: &signer, user1: &signer, user2: &signer, user3: &signer) acquires FreeCoins, LendingPoolReserve, LendingProtocol, User { 547 | test_init(admin, user1, user2, user3); 548 | // user3 tries to borrow a lot should fail 549 | borrow(user3, 800000); 550 | } 551 | 552 | #[test(admin=@hippo_tutorial, user1=@0x1001, user2=@0x1002, user3=@0x1003)] 553 | public entry fun test_withdraw_success(admin: &signer, user1: &signer, user2: &signer, user3: &signer) acquires FreeCoins, LendingPoolReserve, LendingProtocol, User { 554 | test_init(admin, user1, user2, user3); 555 | // user2 tries to withdraw a lot should succeed 556 | withdraw(user2, 100); 557 | } 558 | 559 | #[expected_failure] 560 | #[test(admin=@hippo_tutorial, user1=@0x1001, user2=@0x1002, user3=@0x1003)] 561 | public entry fun test_withdraw_failure(admin: &signer, user1: &signer, user2: &signer, user3: &signer) acquires FreeCoins, LendingPoolReserve, LendingProtocol, User { 562 | test_init(admin, user1, user2, user3); 563 | // user3 tries to withdraw a lot should fail 564 | withdraw(user3, 100); 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /sources/simple_map.move: -------------------------------------------------------------------------------- 1 | /// This module provides a solution for sorted maps, that is it has the properties that 2 | /// 1) Keys point to Values 3 | /// 2) Each Key must be unique 4 | /// 3) A Key can be found within O(Log N) time 5 | /// 4) The data is stored as sorted by Key 6 | /// 5) Adds and removals take O(N) time 7 | module hippo_tutorial::simple_map { 8 | use std::error; 9 | use std::option; 10 | use std::vector; 11 | use aptos_std::comparator; 12 | 13 | /// Map key already exists 14 | const EKEY_ALREADY_EXISTS: u64 = 1; 15 | /// Map key is not found 16 | const EKEY_NOT_FOUND: u64 = 2; 17 | 18 | struct SimpleMap has copy, drop, store { 19 | data: vector>, 20 | } 21 | 22 | struct Element has copy, drop, store { 23 | key: Key, 24 | value: Value, 25 | } 26 | 27 | public fun length(map: &SimpleMap): u64 { 28 | vector::length(&map.data) 29 | } 30 | 31 | public fun create(): SimpleMap { 32 | SimpleMap { 33 | data: vector::empty(), 34 | } 35 | } 36 | 37 | public fun borrow( 38 | map: &SimpleMap, 39 | key: &Key, 40 | ): &Value { 41 | let (maybe_idx, _) = find(map, key); 42 | assert!(option::is_some(&maybe_idx), error::invalid_argument(EKEY_NOT_FOUND)); 43 | let idx = option::extract(&mut maybe_idx); 44 | &vector::borrow(&map.data, idx).value 45 | } 46 | 47 | public fun borrow_by_index( 48 | map: &SimpleMap, 49 | index: u64 50 | ): &Value { 51 | &vector::borrow(&map.data, index).value 52 | } 53 | 54 | public fun borrow_mut( 55 | map: &mut SimpleMap, 56 | key: &Key, 57 | ): &mut Value { 58 | let (maybe_idx, _) = find(map, key); 59 | assert!(option::is_some(&maybe_idx), error::invalid_argument(EKEY_NOT_FOUND)); 60 | let idx = option::extract(&mut maybe_idx); 61 | &mut vector::borrow_mut(&mut map.data, idx).value 62 | } 63 | 64 | public fun contains_key( 65 | map: &SimpleMap, 66 | key: &Key, 67 | ): bool { 68 | let (maybe_idx, _) = find(map, key); 69 | option::is_some(&maybe_idx) 70 | } 71 | 72 | public fun destroy_empty(map: SimpleMap) { 73 | let SimpleMap { data } = map; 74 | vector::destroy_empty(data); 75 | } 76 | 77 | public fun add( 78 | map: &mut SimpleMap, 79 | key: Key, 80 | value: Value, 81 | ) { 82 | let (maybe_idx, maybe_placement) = find(map, &key); 83 | assert!(option::is_none(&maybe_idx), error::invalid_argument(EKEY_ALREADY_EXISTS)); 84 | 85 | // Append to the end and then swap elements until the list is ordered again 86 | vector::push_back(&mut map.data, Element { key, value }); 87 | 88 | let placement = option::extract(&mut maybe_placement); 89 | let end = vector::length(&map.data) - 1; 90 | while (placement < end) { 91 | vector::swap(&mut map.data, placement, end); 92 | placement = placement + 1; 93 | }; 94 | } 95 | 96 | public fun remove( 97 | map: &mut SimpleMap, 98 | key: &Key, 99 | ): (Key, Value) { 100 | let (maybe_idx, _) = find(map, key); 101 | assert!(option::is_some(&maybe_idx), error::invalid_argument(EKEY_NOT_FOUND)); 102 | 103 | let placement = option::extract(&mut maybe_idx); 104 | let end = vector::length(&map.data) - 1; 105 | 106 | while (placement < end) { 107 | vector::swap(&mut map.data, placement, placement + 1); 108 | placement = placement + 1; 109 | }; 110 | 111 | let Element { key, value } = vector::pop_back(&mut map.data); 112 | (key, value) 113 | } 114 | 115 | fun find( 116 | map: &SimpleMap, 117 | key: &Key, 118 | ): (option::Option, option::Option) { 119 | let length = vector::length(&map.data); 120 | 121 | if (length == 0) { 122 | return (option::none(), option::some(0)) 123 | }; 124 | 125 | let left = 0; 126 | let right = length; 127 | 128 | while (left != right) { 129 | let mid = left + (right - left) / 2; 130 | let potential_key = &vector::borrow(&map.data, mid).key; 131 | if (comparator::is_smaller_than(&comparator::compare(potential_key, key))) { 132 | left = mid + 1; 133 | } else { 134 | right = mid; 135 | }; 136 | }; 137 | 138 | if (left != length && key == &vector::borrow(&map.data, left).key) { 139 | (option::some(left), option::none()) 140 | } else { 141 | (option::none(), option::some(left)) 142 | } 143 | } 144 | 145 | #[test] 146 | public fun add_remove_many() { 147 | let map = create(); 148 | 149 | assert!(length(&map) == 0, 0); 150 | assert!(!contains_key(&map, &3), 1); 151 | add(&mut map, 3, 1); 152 | assert!(length(&map) == 1, 2); 153 | assert!(contains_key(&map, &3), 3); 154 | assert!(borrow(&map, &3) == &1, 4); 155 | *borrow_mut(&mut map, &3) = 2; 156 | assert!(borrow(&map, &3) == &2, 5); 157 | 158 | assert!(!contains_key(&map, &2), 6); 159 | add(&mut map, 2, 5); 160 | assert!(length(&map) == 2, 7); 161 | assert!(contains_key(&map, &2), 8); 162 | assert!(borrow(&map, &2) == &5, 9); 163 | *borrow_mut(&mut map, &2) = 9; 164 | assert!(borrow(&map, &2) == &9, 10); 165 | 166 | remove(&mut map, &2); 167 | assert!(length(&map) == 1, 11); 168 | assert!(!contains_key(&map, &2), 12); 169 | assert!(borrow(&map, &3) == &2, 13); 170 | 171 | remove(&mut map, &3); 172 | assert!(length(&map) == 0, 14); 173 | assert!(!contains_key(&map, &3), 15); 174 | 175 | destroy_empty(map); 176 | } 177 | 178 | #[test] 179 | public fun test_several() { 180 | let map = create(); 181 | add(&mut map, 6, 6); 182 | add(&mut map, 1, 1); 183 | add(&mut map, 5, 5); 184 | add(&mut map, 2, 2); 185 | add(&mut map, 3, 3); 186 | add(&mut map, 0, 0); 187 | add(&mut map, 7, 7); 188 | add(&mut map, 4, 4); 189 | 190 | let idx = 0; 191 | while (idx < vector::length(&map.data)) { 192 | assert!(idx == vector::borrow(&map.data, idx).key, idx); 193 | idx = idx + 1; 194 | }; 195 | 196 | remove(&mut map, &0); 197 | remove(&mut map, &1); 198 | remove(&mut map, &2); 199 | remove(&mut map, &3); 200 | remove(&mut map, &4); 201 | remove(&mut map, &5); 202 | remove(&mut map, &6); 203 | remove(&mut map, &7); 204 | 205 | destroy_empty(map); 206 | } 207 | 208 | #[test] 209 | #[expected_failure] 210 | public fun add_twice() { 211 | let map = create(); 212 | add(&mut map, 3, 1); 213 | add(&mut map, 3, 1); 214 | 215 | remove(&mut map, &3); 216 | destroy_empty(map); 217 | } 218 | 219 | #[test] 220 | #[expected_failure] 221 | public fun remove_twice() { 222 | let map = create(); 223 | add(&mut map, 3, 1); 224 | remove(&mut map, &3); 225 | remove(&mut map, &3); 226 | 227 | destroy_empty(map); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /typescript/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | src 4 | jest* 5 | package* 6 | ts* 7 | -------------------------------------------------------------------------------- /typescript/src/cli2.ts: -------------------------------------------------------------------------------- 1 | import { AptosParserRepo, getTypeTagFullname, StructTag, parseTypeTagOrThrow, u8, u64, u128, print, strToU8, u8str, DummyCache } from "@manahippo/move-to-ts"; 2 | import { AptosAccount, AptosClient, HexString, Types } from "aptos"; 3 | import { Command } from "commander"; 4 | import { App } from "./"; 5 | import * as fs from "fs"; 6 | import * as yaml from "yaml"; 7 | import * as Hippo_tutorial from './hippo_tutorial'; 8 | 9 | export const readConfig = (program: Command) => { 10 | const {config, profile} = program.opts(); 11 | const ymlContent = fs.readFileSync(config, {encoding: "utf-8"}); 12 | const result = yaml.parse(ymlContent); 13 | //console.log(result); 14 | if (!result.profiles) { 15 | throw new Error("Expect a profiles to be present in yaml config"); 16 | } 17 | if (!result.profiles[profile]) { 18 | throw new Error(`Expect a ${profile} profile to be present in yaml config`); 19 | } 20 | const url = result.profiles[profile].rest_url; 21 | const privateKeyStr = result.profiles[profile].private_key; 22 | if (!url) { 23 | throw new Error(`Expect rest_url to be present in ${profile} profile`); 24 | } 25 | if (!privateKeyStr) { 26 | throw new Error(`Expect private_key to be present in ${profile} profile`); 27 | } 28 | const privateKey = new HexString(privateKeyStr); 29 | const client = new AptosClient(result.profiles[profile].rest_url); 30 | const account = new AptosAccount(privateKey.toUint8Array()); 31 | console.log(`Using address ${account.address().hex()}`); 32 | return {client, account}; 33 | } 34 | 35 | const program = new Command(); 36 | 37 | program 38 | .name('cli') 39 | .description('Move TS CLI generated by move-to-ts') 40 | .requiredOption('-c, --config ', 'path to your aptos config.yml (generated with "aptos init")') 41 | .option('-p, --profile ', 'aptos config profile to use', 'default') 42 | 43 | 44 | const check_user = async () => { 45 | const {client, account} = readConfig(program); 46 | const app = new App(client).hippo_tutorial.lend2; 47 | const userAddr = account.address(); 48 | const protocolAddr = app.moduleAddress; 49 | const user = await app.loadUser(userAddr); 50 | const protocol = await app.loadLendingProtocol(protocolAddr, false); 51 | print(user.user_get_limits(protocol)); 52 | // send tx 53 | await app.withdraw(account, u64(1000000), [app.FakeBTC.getTag()]); 54 | } 55 | 56 | program 57 | .command("check-user") 58 | .action(check_user); 59 | 60 | 61 | const check_user_global = async () => { 62 | const {client, account} = readConfig(program); 63 | const app = new App(client).hippo_tutorial.lend2; 64 | const userAddr = account.address(); 65 | await app.loadUser(userAddr); 66 | await app.loadLendingProtocol(app.moduleAddress, false, true); 67 | print(app.app_global_get_user_limits(userAddr)); 68 | } 69 | 70 | program 71 | .command("check-user-global") 72 | .action(check_user_global); 73 | 74 | 75 | const check_user_async = async () => { 76 | const {client, account} = readConfig(program); 77 | const app = new App(client).hippo_tutorial.lend2; 78 | const userAddr = account.address(); 79 | print(await app.app_global_get_user_limits(userAddr)); 80 | } 81 | 82 | program 83 | .command("check-user-async") 84 | .action(check_user_async); 85 | 86 | program.parse(); 87 | --------------------------------------------------------------------------------