├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── .gitignore └── _config.yml ├── lerna.json ├── package.json ├── packages ├── associated-token │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── borsh │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── common │ ├── package.json │ ├── src │ │ ├── connection.ts │ │ ├── index.ts │ │ ├── provider.ts │ │ ├── simulate-transaction.ts │ │ └── token.ts │ ├── test │ │ └── example.spec.ts │ └── tsconfig.json ├── pool │ ├── .eslintignore │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── admin-controlled-pool.ts │ │ ├── index.ts │ │ ├── instructions.ts │ │ ├── schema.test.ts │ │ ├── schema.ts │ │ ├── scripts │ │ │ └── create-pool-and-do-stuff-with-it.ts │ │ ├── simulate-transaction.ts │ │ └── transactions.ts │ └── tsconfig.json ├── serum │ ├── .eslintignore │ ├── .eslintrc │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── shell │ ├── src │ │ ├── error.ts │ │ ├── fees.ts │ │ ├── index.js │ │ ├── instructions.js │ │ ├── instructions.test.js │ │ ├── layout.js │ │ ├── market-proxy │ │ │ ├── index.ts │ │ │ └── middleware.ts │ │ ├── market.test.js │ │ ├── market.ts │ │ ├── markets.json │ │ ├── queue.ts │ │ ├── slab.test.js │ │ ├── slab.ts │ │ ├── token-instructions.js │ │ ├── token-mints.json │ │ └── tokens_and_markets.ts │ └── tsconfig.json ├── spl-token-swap │ ├── CHANGELOG.md │ ├── package-lock.json │ ├── package.json │ ├── shell │ ├── src │ │ ├── index.ts │ │ ├── instructions.ts │ │ ├── pools.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── test │ │ └── example.spec.ts │ └── tsconfig.json ├── swap │ ├── .eslintignore │ ├── .eslintrc │ ├── README.md │ ├── examples │ │ └── swap.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── idl.ts │ │ ├── index.ts │ │ ├── swap-markets.ts │ │ └── utils.ts │ └── tsconfig.json ├── token │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── instructions.ts │ │ ├── metadata.ts │ │ └── state.ts │ └── tsconfig.json └── tokens │ ├── package.json │ ├── rollup │ ├── rollup.config.browser.ts │ └── rollup.config.ts │ ├── src │ ├── devnet.json │ ├── index.ts │ ├── mainnet-beta.json │ └── testnet.json │ └── tsconfig.json ├── tsconfig.json ├── types └── buffer-layout │ └── index.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier", 8 | "prettier/@typescript-eslint" 9 | ], 10 | "env": { 11 | "node": true, 12 | "jest": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "no-constant-condition": ["error", { "checkLoops": false }], 20 | "no-empty-function": "off", 21 | "@typescript-eslint/ban-ts-comment": "off", 22 | "@typescript-eslint/no-explicit-any": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | dist/ 4 | lib/ 5 | deploy/ 6 | docs/lockup-ui/ 7 | .DS_Store 8 | *~ 9 | .idea 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | private/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 12 3 | dist: bionic 4 | cache: yarn 5 | before_script: 6 | - yarn build 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Serum Foundation 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

serum-ts

5 | 6 |

7 | Project Serum Monorepo 8 |

9 | 10 |

11 | Build Status 12 | Discord Chat 13 | License 14 |

15 | 16 |

17 | Website 18 | | 19 | Academy 20 | | 21 | Awesome 22 | | 23 | DEX 24 | | 25 | Rust 26 |

27 |
28 | 29 | ## Packages 30 | 31 | | Package | Version | Description | 32 | | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | 33 | | [`@project-serum/borsh`](/packages/borsh) | [![npm](https://img.shields.io/npm/v/@project-serum/borsh.svg)](https://www.npmjs.com/package/@project-serum/borsh) | Borsh serialization primitives | 34 | | [`@project-serum/common`](/packages/common) | [![npm](https://img.shields.io/npm/v/@project-serum/common.svg)](https://www.npmjs.com/package/@project-serum/common) | Common utilities | 35 | | [`@project-serum/serum`](/packages/serum) | [![npm](https://img.shields.io/npm/v/@project-serum/serum.svg)](https://www.npmjs.com/package/@project-serum/serum) | Library for interacting with the Serum DEX | 36 | | [`@project-serum/pool`](/packages/pool) | [![npm](https://img.shields.io/npm/v/@project-serum/pool.svg)](https://www.npmjs.com/package/@project-serum/pool) | Client for interacting with Pools | 37 | | [`@project-serum/spl-token-swap`](/packages/spl-token-swap) | [![npm](https://img.shields.io/npm/v/@project-serum/spl-token-swap.svg)](https://www.npmjs.com/package/@project-serum/spl-token-swap) | Client for interacting with the SPL Token Swap Program | 38 | | [`@project-serum/swap`](/packages/swap) | [![npm](https://img.shields.io/npm/v/@project-serum/swap.svg)](https://www.npmjs.com/package/@project-serum/swap) | Client for swapping on the Serum DEX | 39 | | [`@project-serum/tokens`](/packages/tokens) | [![npm](https://img.shields.io/npm/v/@project-serum/tokens.svg)](https://www.npmjs.com/package/@project-serum/tokens) | Solana token addresses | 40 | 41 | ## Contributing 42 | 43 | ### Installing 44 | 45 | To get started first install the required build tools: 46 | 47 | ``` 48 | npm install -g lerna 49 | npm install -g yarn 50 | ``` 51 | 52 | Then bootstrap the workspace: 53 | 54 | ``` 55 | yarn 56 | ``` 57 | 58 | ### Building 59 | 60 | To build the workspace: 61 | 62 | ``` 63 | yarn build 64 | ``` 65 | 66 | ### Testing 67 | 68 | To run all tests: 69 | 70 | ``` 71 | yarn test 72 | ``` 73 | 74 | ### Linting 75 | 76 | To lint: 77 | 78 | ``` 79 | yarn lint 80 | ``` 81 | 82 | To apply lint fixes: 83 | 84 | ``` 85 | yarn lint:fix 86 | ``` 87 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /associated-token 2 | /borsh 3 | /pool 4 | /token 5 | /stake-ui 6 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - "_*_.html" 3 | - "_*_.*.html" 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "3.4.3", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "packages": ["packages/*"], 6 | "version": "independent", 7 | "publishConfig": { 8 | "access": "public", 9 | "registry": "https://registry.npmjs.org" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "keywords": [], 4 | "workspaces": { 5 | "packages": [ 6 | "packages/*" 7 | ] 8 | }, 9 | "license": "Apache-2.0", 10 | "engines": { 11 | "node": ">=6.0.0" 12 | }, 13 | "scripts": { 14 | "build": "lerna run build", 15 | "lint": "eslint 'packages/*/{src,test}/**/*.ts' && prettier -c 'packages/*/{src,test}/**/*.ts'", 16 | "lint:fix": "eslint --fix 'packages/*/{src,test}/**/*.ts' && prettier --write 'packages/*/{src,test}/**/*.ts'", 17 | "deploy": "run-s deploy:docs build deploy:apps && gh-pages -d docs", 18 | "deploy:docs": "lerna run docs", 19 | "deploy:apps": "lerna run deploy:app", 20 | "test": "lerna run test --concurrency 1 --stream" 21 | }, 22 | "lint-staged": { 23 | "packages/*/{src,test}/**/*.ts": [ 24 | "prettier --write" 25 | ] 26 | }, 27 | "husky": { 28 | "hooks": { 29 | "pre-commit": "lint-staged" 30 | } 31 | }, 32 | "prettier": { 33 | "arrowParens": "avoid", 34 | "semi": true, 35 | "singleQuote": true, 36 | "trailingComma": "all" 37 | }, 38 | "commitlint": { 39 | "extends": [ 40 | "@commitlint/config-conventional" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "@commitlint/cli": "^8.2.0", 45 | "@commitlint/config-conventional": "^8.2.0", 46 | "@types/jest": "^26.0.15", 47 | "@typescript-eslint/eslint-plugin": "^4.6.0", 48 | "@typescript-eslint/parser": "^4.6.0", 49 | "eslint": "^7.12.1", 50 | "eslint-config-prettier": "^6.15.0", 51 | "gh-pages": "^3.1.0", 52 | "husky": "^4.3.0", 53 | "jest": "26.6.0", 54 | "jest-config": "26.6.0", 55 | "lerna": "3.22.1", 56 | "lint-staged": "^10.5.0", 57 | "prettier": "^2.1.2", 58 | "rollup": "^1.23.1", 59 | "rollup-plugin-commonjs": "^10.1.0", 60 | "rollup-plugin-json": "^4.0.0", 61 | "rollup-plugin-node-resolve": "^5.2.0", 62 | "rollup-plugin-sourcemaps": "^0.4.2", 63 | "rollup-plugin-terser": "^5.1.3", 64 | "rollup-plugin-typescript2": "^0.25.2", 65 | "ts-jest": "^26.4.3", 66 | "ts-node": "^9.0.0", 67 | "typescript": "^4.0.5" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/associated-token/README.md: -------------------------------------------------------------------------------- 1 | JavaScript library to interact with SPL Associated Token Accounts. 2 | 3 | [API Reference](https://project-serum.github.io/serum-ts/associated-token/modules/_index_.html) 4 | -------------------------------------------------------------------------------- /packages/associated-token/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-serum/associated-token", 3 | "version": "0.1.1", 4 | "description": "Library for interacting with SPL Associated Token Accounts", 5 | "repository": "project-serum/serum-ts", 6 | "main": "dist/lib/index.js", 7 | "types": "dist/lib/index.d.ts", 8 | "exports": { 9 | ".": "./dist/lib/index.js" 10 | }, 11 | "license": "Apache-2.0", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "engines": { 16 | "node": ">=10" 17 | }, 18 | "scripts": { 19 | "build": "tsc", 20 | "docs": "typedoc --out ../../docs/associated-token --mode library --composite false --rootDir src src/index.ts src/*.d.ts", 21 | "test": "", 22 | "clean": "rm -rf dist", 23 | "prepare": "run-s clean build" 24 | }, 25 | "peerDependencies": { 26 | "@solana/web3.js": "^0.90.0" 27 | }, 28 | "devDependencies": { 29 | "@solana/web3.js": "^0.90.0" 30 | }, 31 | "files": [ 32 | "dist" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/associated-token/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PublicKey, 3 | SystemProgram, 4 | SYSVAR_RENT_PUBKEY, 5 | TransactionInstruction, 6 | } from '@solana/web3.js'; 7 | 8 | const TOKEN_PROGRAM_ID = new PublicKey( 9 | 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 10 | ); 11 | 12 | /** Program ID for the associated token account program. */ 13 | export const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey( 14 | 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', 15 | ); 16 | 17 | /** 18 | * Derives the associated token address for the given wallet address and token mint. 19 | * @param owner Wallet address 20 | * @param mint Mint address 21 | */ 22 | export async function getAssociatedTokenAddress( 23 | owner: PublicKey, 24 | mint: PublicKey, 25 | ): Promise { 26 | const [address] = await PublicKey.findProgramAddress( 27 | [owner.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], 28 | ASSOCIATED_TOKEN_PROGRAM_ID, 29 | ); 30 | return address; 31 | } 32 | 33 | /** 34 | * Instruction to create the associated token address for the given wallet address and token mint. 35 | * 36 | * @param payer Account to use to pay for fees 37 | * @param owner Wallet address for the new associated token address 38 | * @param mint Mint address for the new associated token address 39 | */ 40 | export async function createAssociatedTokenAccount( 41 | payer: PublicKey, 42 | owner: PublicKey, 43 | mint: PublicKey, 44 | ): Promise { 45 | const associatedTokenAddress = await getAssociatedTokenAddress(owner, mint); 46 | return new TransactionInstruction({ 47 | keys: [ 48 | { pubkey: payer, isSigner: true, isWritable: true }, 49 | { pubkey: associatedTokenAddress, isSigner: false, isWritable: true }, 50 | { pubkey: owner, isSigner: false, isWritable: false }, 51 | { pubkey: mint, isSigner: false, isWritable: false }, 52 | { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, 53 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, 54 | { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, 55 | ], 56 | programId: ASSOCIATED_TOKEN_PROGRAM_ID, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /packages/associated-token/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | 6 | "outDir": "./dist/lib", 7 | "rootDir": "./src", 8 | 9 | "composite": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | 14 | "strict": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "typeRoots": ["../../types/", "../../node_modules/@types"] 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["src/**/*.test.ts", "**/node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/borsh/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-serum/borsh", 3 | "version": "0.2.5", 4 | "description": "Serum Borsh", 5 | "main": "dist/lib/index.js", 6 | "types": "dist/lib/index.d.ts", 7 | "exports": { 8 | ".": "./dist/lib/index.js" 9 | }, 10 | "license": "Apache-2.0", 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "engines": { 15 | "node": ">=10" 16 | }, 17 | "scripts": { 18 | "build": "tsc", 19 | "docs": "typedoc --out ../../docs/borsh --mode library --composite false --rootDir src src/index.ts src/*.d.ts", 20 | "test": "", 21 | "clean": "rm -rf dist", 22 | "prepare": "run-s clean build" 23 | }, 24 | "dependencies": { 25 | "bn.js": "^5.1.2", 26 | "buffer-layout": "^1.2.0" 27 | }, 28 | "peerDependencies": { 29 | "@solana/web3.js": "^1.2.0" 30 | }, 31 | "files": [ 32 | "dist" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/borsh/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | blob, 3 | Layout as LayoutCls, 4 | offset, 5 | seq, 6 | struct, 7 | u32, 8 | u8, 9 | union, 10 | } from 'buffer-layout'; 11 | import { PublicKey } from '@solana/web3.js'; 12 | import BN from 'bn.js'; 13 | export { 14 | u8, 15 | s8 as i8, 16 | u16, 17 | s16 as i16, 18 | u32, 19 | s32 as i32, 20 | f32, 21 | f64, 22 | struct, 23 | } from 'buffer-layout'; 24 | 25 | export interface Layout { 26 | span: number; 27 | property?: string; 28 | 29 | decode(b: Buffer, offset?: number): T; 30 | 31 | encode(src: T, b: Buffer, offset?: number): number; 32 | 33 | getSpan(b: Buffer, offset?: number): number; 34 | 35 | replicate(name: string): this; 36 | } 37 | 38 | class BNLayout extends LayoutCls { 39 | blob: Layout; 40 | signed: boolean; 41 | 42 | constructor(span: number, signed: boolean, property?: string) { 43 | super(span, property); 44 | this.blob = blob(span); 45 | this.signed = signed; 46 | } 47 | 48 | decode(b: Buffer, offset = 0) { 49 | const num = new BN(this.blob.decode(b, offset), 10, 'le'); 50 | if (this.signed) { 51 | return num.fromTwos(this.span * 8).clone(); 52 | } 53 | return num; 54 | } 55 | 56 | encode(src: BN, b: Buffer, offset = 0) { 57 | if (this.signed) { 58 | src = src.toTwos(this.span * 8); 59 | } 60 | return this.blob.encode( 61 | src.toArrayLike(Buffer, 'le', this.span), 62 | b, 63 | offset, 64 | ); 65 | } 66 | } 67 | 68 | export function u64(property?: string): Layout { 69 | return new BNLayout(8, false, property); 70 | } 71 | 72 | export function i64(property?: string): Layout { 73 | return new BNLayout(8, true, property); 74 | } 75 | 76 | export function u128(property?: string): Layout { 77 | return new BNLayout(16, false, property); 78 | } 79 | 80 | export function i128(property?: string): Layout { 81 | return new BNLayout(16, true, property); 82 | } 83 | 84 | class WrappedLayout extends LayoutCls { 85 | layout: Layout; 86 | decoder: (data: T) => U; 87 | encoder: (src: U) => T; 88 | 89 | constructor( 90 | layout: Layout, 91 | decoder: (data: T) => U, 92 | encoder: (src: U) => T, 93 | property?: string, 94 | ) { 95 | super(layout.span, property); 96 | this.layout = layout; 97 | this.decoder = decoder; 98 | this.encoder = encoder; 99 | } 100 | 101 | decode(b: Buffer, offset?: number): U { 102 | return this.decoder(this.layout.decode(b, offset)); 103 | } 104 | 105 | encode(src: U, b: Buffer, offset?: number): number { 106 | return this.layout.encode(this.encoder(src), b, offset); 107 | } 108 | 109 | getSpan(b: Buffer, offset?: number): number { 110 | return this.layout.getSpan(b, offset); 111 | } 112 | } 113 | 114 | export function publicKey(property?: string): Layout { 115 | return new WrappedLayout( 116 | blob(32), 117 | (b: Buffer) => new PublicKey(b), 118 | (key: PublicKey) => key.toBuffer(), 119 | property, 120 | ); 121 | } 122 | 123 | class OptionLayout extends LayoutCls { 124 | layout: Layout; 125 | discriminator: Layout; 126 | 127 | constructor(layout: Layout, property?: string) { 128 | super(-1, property); 129 | this.layout = layout; 130 | this.discriminator = u8(); 131 | } 132 | 133 | encode(src: T | null, b: Buffer, offset = 0): number { 134 | if (src === null || src === undefined) { 135 | return this.discriminator.encode(0, b, offset); 136 | } 137 | this.discriminator.encode(1, b, offset); 138 | return this.layout.encode(src, b, offset + 1) + 1; 139 | } 140 | 141 | decode(b: Buffer, offset = 0): T | null { 142 | const discriminator = this.discriminator.decode(b, offset); 143 | if (discriminator === 0) { 144 | return null; 145 | } else if (discriminator === 1) { 146 | return this.layout.decode(b, offset + 1); 147 | } 148 | throw new Error('Invalid option ' + this.property); 149 | } 150 | 151 | getSpan(b: Buffer, offset = 0): number { 152 | const discriminator = this.discriminator.decode(b, offset); 153 | if (discriminator === 0) { 154 | return 1; 155 | } else if (discriminator === 1) { 156 | return this.layout.getSpan(b, offset + 1) + 1; 157 | } 158 | throw new Error('Invalid option ' + this.property); 159 | } 160 | } 161 | 162 | export function option( 163 | layout: Layout, 164 | property?: string, 165 | ): Layout { 166 | return new OptionLayout(layout, property); 167 | } 168 | 169 | export function bool(property?: string): Layout { 170 | return new WrappedLayout(u8(), decodeBool, encodeBool, property); 171 | } 172 | 173 | function decodeBool(value: number): boolean { 174 | if (value === 0) { 175 | return false; 176 | } else if (value === 1) { 177 | return true; 178 | } 179 | throw new Error('Invalid bool: ' + value); 180 | } 181 | 182 | function encodeBool(value: boolean): number { 183 | return value ? 1 : 0; 184 | } 185 | 186 | export function vec( 187 | elementLayout: Layout, 188 | property?: string, 189 | ): Layout { 190 | const length = u32('length'); 191 | const layout: Layout<{ values: T[] }> = struct([ 192 | length, 193 | seq(elementLayout, offset(length, -length.span), 'values'), 194 | ]); 195 | return new WrappedLayout( 196 | layout, 197 | ({ values }) => values, 198 | values => ({ values }), 199 | property, 200 | ); 201 | } 202 | 203 | export function tagged( 204 | tag: BN, 205 | layout: Layout, 206 | property?: string, 207 | ): Layout { 208 | const wrappedLayout: Layout<{ tag: BN; data: T }> = struct([ 209 | u64('tag'), 210 | layout.replicate('data'), 211 | ]); 212 | 213 | function decodeTag({ tag: receivedTag, data }: { tag: BN; data: T }) { 214 | if (!receivedTag.eq(tag)) { 215 | throw new Error( 216 | 'Invalid tag, expected: ' + 217 | tag.toString('hex') + 218 | ', got: ' + 219 | receivedTag.toString('hex'), 220 | ); 221 | } 222 | return data; 223 | } 224 | 225 | return new WrappedLayout( 226 | wrappedLayout, 227 | decodeTag, 228 | data => ({ tag, data }), 229 | property, 230 | ); 231 | } 232 | 233 | export function vecU8(property?: string): Layout { 234 | const length = u32('length'); 235 | const layout: Layout<{ data: Buffer }> = struct([ 236 | length, 237 | blob(offset(length, -length.span), 'data'), 238 | ]); 239 | return new WrappedLayout( 240 | layout, 241 | ({ data }) => data, 242 | data => ({ data }), 243 | property, 244 | ); 245 | } 246 | 247 | export function str(property?: string): Layout { 248 | return new WrappedLayout( 249 | vecU8(), 250 | data => data.toString('utf-8'), 251 | s => Buffer.from(s, 'utf-8'), 252 | property, 253 | ); 254 | } 255 | 256 | export interface EnumLayout extends Layout { 257 | registry: Record>; 258 | } 259 | 260 | export function rustEnum( 261 | variants: Layout[], 262 | property?: string, 263 | discriminant?: Layout, 264 | ): EnumLayout { 265 | const unionLayout = union(discriminant ?? u8(), property); 266 | variants.forEach((variant, index) => 267 | unionLayout.addVariant(index, variant, variant.property), 268 | ); 269 | return unionLayout; 270 | } 271 | 272 | export function array( 273 | elementLayout: Layout, 274 | length: number, 275 | property?: string, 276 | ): Layout { 277 | const layout: Layout<{ values: T[] }> = struct([ 278 | seq(elementLayout, length, 'values'), 279 | ]); 280 | return new WrappedLayout( 281 | layout, 282 | ({ values }) => values, 283 | values => ({ values }), 284 | property, 285 | ); 286 | } 287 | 288 | class MapEntryLayout extends LayoutCls<[K, V]> { 289 | keyLayout: Layout; 290 | valueLayout: Layout; 291 | 292 | constructor(keyLayout: Layout, valueLayout: Layout, property?: string) { 293 | super(keyLayout.span + valueLayout.span, property); 294 | this.keyLayout = keyLayout; 295 | this.valueLayout = valueLayout; 296 | } 297 | 298 | decode(b: Buffer, offset?: number): [K, V] { 299 | offset = offset || 0; 300 | const key = this.keyLayout.decode(b, offset); 301 | const value = this.valueLayout.decode( 302 | b, 303 | offset + this.keyLayout.getSpan(b, offset), 304 | ); 305 | return [key, value]; 306 | } 307 | 308 | encode(src: [K, V], b: Buffer, offset?: number): number { 309 | offset = offset || 0; 310 | const keyBytes = this.keyLayout.encode(src[0], b, offset); 311 | const valueBytes = this.valueLayout.encode(src[1], b, offset + keyBytes); 312 | return keyBytes + valueBytes; 313 | } 314 | 315 | getSpan(b: Buffer, offset?: number): number { 316 | return ( 317 | this.keyLayout.getSpan(b, offset) + this.valueLayout.getSpan(b, offset) 318 | ); 319 | } 320 | } 321 | 322 | export function map( 323 | keyLayout: Layout, 324 | valueLayout: Layout, 325 | property?: string, 326 | ): Layout> { 327 | const length = u32('length'); 328 | const layout: Layout<{ values: [K, V][] }> = struct([ 329 | length, 330 | seq( 331 | new MapEntryLayout(keyLayout, valueLayout), 332 | offset(length, -length.span), 333 | 'values', 334 | ), 335 | ]); 336 | return new WrappedLayout( 337 | layout, 338 | ({ values }) => new Map(values), 339 | values => ({ values: Array.from(values.entries()) }), 340 | property, 341 | ); 342 | } 343 | -------------------------------------------------------------------------------- /packages/borsh/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | 6 | "outDir": "./dist/lib", 7 | "rootDir": "./src", 8 | 9 | "composite": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | 14 | "strict": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "typeRoots": ["../../types/", "../../node_modules/@types"] 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["src/**/*.test.ts", "**/node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-serum/common", 3 | "version": "0.0.1-beta.3", 4 | "description": "Serum common utilities", 5 | "main": "dist/lib/index.js", 6 | "types": "dist/lib/index.d.ts", 7 | "exports": { 8 | ".": "./dist/lib/index.js" 9 | }, 10 | "license": "Apache-2.0", 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "engines": { 15 | "node": ">=10" 16 | }, 17 | "scripts": { 18 | "build": "tsc", 19 | "watch": "tsc --watch", 20 | "test": "jest test", 21 | "clean": "rm -rf dist", 22 | "prepare": "run-s clean build" 23 | }, 24 | "dependencies": { 25 | "@project-serum/serum": "^0.13.21", 26 | "bn.js": "^5.1.2", 27 | "superstruct": "0.8.3" 28 | }, 29 | "peerDependencies": { 30 | "@solana/web3.js": "^0.90.0" 31 | }, 32 | "files": [ 33 | "dist" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/common/src/connection.ts: -------------------------------------------------------------------------------- 1 | import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'; 2 | import { struct } from 'superstruct'; 3 | import assert from 'assert'; 4 | 5 | export async function getMultipleSolanaAccounts( 6 | connection: Connection, 7 | publicKeys: PublicKey[], 8 | ): Promise }>> { 9 | const args = [publicKeys.map(k => k.toBase58()), { commitment: 'recent' }]; 10 | // @ts-ignore 11 | const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args); 12 | const res = GetMultipleAccountsAndContextRpcResult(unsafeRes); 13 | if (res.error) { 14 | throw new Error( 15 | 'failed to get info about accounts ' + 16 | publicKeys.map(k => k.toBase58()).join(', ') + 17 | ': ' + 18 | res.error.message, 19 | ); 20 | } 21 | assert(typeof res.result !== 'undefined'); 22 | const accounts: Array<{ 23 | executable: any; 24 | owner: PublicKey; 25 | lamports: any; 26 | data: Buffer; 27 | }> = []; 28 | for (const account of res.result.value) { 29 | let value: { 30 | executable: any; 31 | owner: PublicKey; 32 | lamports: any; 33 | data: Buffer; 34 | } | null = null; 35 | if (res.result.value) { 36 | const { executable, owner, lamports, data } = account; 37 | assert(data[1] === 'base64'); 38 | value = { 39 | executable, 40 | owner: new PublicKey(owner), 41 | lamports, 42 | data: Buffer.from(data[0], 'base64'), 43 | }; 44 | } 45 | if (value === null) { 46 | throw new Error('Invalid response'); 47 | } 48 | accounts.push(value); 49 | } 50 | return accounts.map((account, idx) => { 51 | return { 52 | publicKey: publicKeys[idx], 53 | account, 54 | }; 55 | }); 56 | } 57 | 58 | function jsonRpcResult(resultDescription: any) { 59 | const jsonRpcVersion = struct.literal('2.0'); 60 | return struct.union([ 61 | struct({ 62 | jsonrpc: jsonRpcVersion, 63 | id: 'string', 64 | error: 'any', 65 | }), 66 | struct({ 67 | jsonrpc: jsonRpcVersion, 68 | id: 'string', 69 | error: 'null?', 70 | result: resultDescription, 71 | }), 72 | ]); 73 | } 74 | 75 | function jsonRpcResultAndContext(resultDescription: any) { 76 | return jsonRpcResult({ 77 | context: struct({ 78 | slot: 'number', 79 | }), 80 | value: resultDescription, 81 | }); 82 | } 83 | 84 | const AccountInfoResult = struct({ 85 | executable: 'boolean', 86 | owner: 'string', 87 | lamports: 'number', 88 | data: 'any', 89 | rentEpoch: 'number?', 90 | }); 91 | 92 | export const GetMultipleAccountsAndContextRpcResult = jsonRpcResultAndContext( 93 | struct.array([struct.union(['null', AccountInfoResult])]), 94 | ); 95 | -------------------------------------------------------------------------------- /packages/common/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Account, 3 | SystemProgram, 4 | PublicKey, 5 | Transaction, 6 | TransactionInstruction, 7 | } from '@solana/web3.js'; 8 | import { Provider } from './provider'; 9 | import { 10 | MintInfo, 11 | MintLayout, 12 | AccountInfo, 13 | AccountLayout, 14 | u64, 15 | } from '@solana/spl-token'; 16 | import { TokenInstructions } from '@project-serum/serum'; 17 | import BN from 'bn.js'; 18 | 19 | export * from './provider'; 20 | export * as token from './token'; 21 | export { simulateTransaction } from './simulate-transaction'; 22 | export * as connection from './connection'; 23 | 24 | export const SPL_SHARED_MEMORY_ID = new PublicKey( 25 | 'shmem4EWT2sPdVGvTZCzXXRAURL9G5vpPxNwSeKhHUL', 26 | ); 27 | 28 | export async function createMint( 29 | provider: Provider, 30 | authority?: PublicKey, 31 | decimals?: number, 32 | ): Promise { 33 | if (authority === undefined) { 34 | authority = provider.wallet.publicKey; 35 | } 36 | const mint = new Account(); 37 | const instructions = await createMintInstructions( 38 | provider, 39 | authority, 40 | mint.publicKey, 41 | decimals, 42 | ); 43 | 44 | const tx = new Transaction(); 45 | tx.add(...instructions); 46 | 47 | await provider.send(tx, [mint]); 48 | 49 | return mint.publicKey; 50 | } 51 | 52 | export async function createMintInstructions( 53 | provider: Provider, 54 | authority: PublicKey, 55 | mint: PublicKey, 56 | decimals?: number, 57 | ): Promise { 58 | let instructions = [ 59 | SystemProgram.createAccount({ 60 | fromPubkey: provider.wallet.publicKey, 61 | newAccountPubkey: mint, 62 | space: 82, 63 | lamports: await provider.connection.getMinimumBalanceForRentExemption(82), 64 | programId: TokenInstructions.TOKEN_PROGRAM_ID, 65 | }), 66 | TokenInstructions.initializeMint({ 67 | mint, 68 | decimals: decimals ?? 0, 69 | mintAuthority: authority, 70 | }), 71 | ]; 72 | return instructions; 73 | } 74 | 75 | export async function createMintAndVault( 76 | provider: Provider, 77 | amount: BN, 78 | owner?: PublicKey, 79 | decimals?: number, 80 | ): Promise<[PublicKey, PublicKey]> { 81 | if (owner === undefined) { 82 | owner = provider.wallet.publicKey; 83 | } 84 | const mint = new Account(); 85 | const vault = new Account(); 86 | const tx = new Transaction(); 87 | tx.add( 88 | SystemProgram.createAccount({ 89 | fromPubkey: provider.wallet.publicKey, 90 | newAccountPubkey: mint.publicKey, 91 | space: 82, 92 | lamports: await provider.connection.getMinimumBalanceForRentExemption(82), 93 | programId: TokenInstructions.TOKEN_PROGRAM_ID, 94 | }), 95 | TokenInstructions.initializeMint({ 96 | mint: mint.publicKey, 97 | decimals: decimals ?? 0, 98 | mintAuthority: provider.wallet.publicKey, 99 | }), 100 | SystemProgram.createAccount({ 101 | fromPubkey: provider.wallet.publicKey, 102 | newAccountPubkey: vault.publicKey, 103 | space: 165, 104 | lamports: await provider.connection.getMinimumBalanceForRentExemption( 105 | 165, 106 | ), 107 | programId: TokenInstructions.TOKEN_PROGRAM_ID, 108 | }), 109 | TokenInstructions.initializeAccount({ 110 | account: vault.publicKey, 111 | mint: mint.publicKey, 112 | owner, 113 | }), 114 | TokenInstructions.mintTo({ 115 | mint: mint.publicKey, 116 | destination: vault.publicKey, 117 | amount, 118 | mintAuthority: provider.wallet.publicKey, 119 | }), 120 | ); 121 | await provider.send(tx, [mint, vault]); 122 | return [mint.publicKey, vault.publicKey]; 123 | } 124 | 125 | export async function createTokenAccount( 126 | provider: Provider, 127 | mint: PublicKey, 128 | owner: PublicKey, 129 | ): Promise { 130 | const vault = new Account(); 131 | const tx = new Transaction(); 132 | tx.add( 133 | ...(await createTokenAccountInstrs(provider, vault.publicKey, mint, owner)), 134 | ); 135 | await provider.send(tx, [vault]); 136 | return vault.publicKey; 137 | } 138 | 139 | export async function createTokenAccountInstrs( 140 | provider: Provider, 141 | newAccountPubkey: PublicKey, 142 | mint: PublicKey, 143 | owner: PublicKey, 144 | lamports?: number, 145 | ): Promise { 146 | if (lamports === undefined) { 147 | lamports = await provider.connection.getMinimumBalanceForRentExemption(165); 148 | } 149 | return [ 150 | SystemProgram.createAccount({ 151 | fromPubkey: provider.wallet.publicKey, 152 | newAccountPubkey, 153 | space: 165, 154 | lamports, 155 | programId: TokenInstructions.TOKEN_PROGRAM_ID, 156 | }), 157 | TokenInstructions.initializeAccount({ 158 | account: newAccountPubkey, 159 | mint, 160 | owner, 161 | }), 162 | ]; 163 | } 164 | 165 | export async function createAccountRentExempt( 166 | provider: Provider, 167 | programId: PublicKey, 168 | size: number, 169 | ): Promise { 170 | const acc = new Account(); 171 | const tx = new Transaction(); 172 | tx.add( 173 | SystemProgram.createAccount({ 174 | fromPubkey: provider.wallet.publicKey, 175 | newAccountPubkey: acc.publicKey, 176 | space: size, 177 | lamports: await provider.connection.getMinimumBalanceForRentExemption( 178 | size, 179 | ), 180 | programId, 181 | }), 182 | ); 183 | await provider.send(tx, [acc]); 184 | return acc; 185 | } 186 | 187 | export async function getMintInfo( 188 | provider: Provider, 189 | addr: PublicKey, 190 | ): Promise { 191 | let depositorAccInfo = await provider.connection.getAccountInfo(addr); 192 | if (depositorAccInfo === null) { 193 | throw new Error('Failed to find token account'); 194 | } 195 | return parseMintAccount(depositorAccInfo.data); 196 | } 197 | 198 | export function parseMintAccount(data: Buffer): MintInfo { 199 | const m = MintLayout.decode(data); 200 | m.mintAuthority = new PublicKey(m.mintAuthority); 201 | m.supply = u64.fromBuffer(m.supply); 202 | m.isInitialized = m.state !== 0; 203 | return m; 204 | } 205 | 206 | export async function getTokenAccount( 207 | provider: Provider, 208 | addr: PublicKey, 209 | ): Promise { 210 | let depositorAccInfo = await provider.connection.getAccountInfo(addr); 211 | if (depositorAccInfo === null) { 212 | throw new Error('Failed to find token account'); 213 | } 214 | return parseTokenAccount(depositorAccInfo.data); 215 | } 216 | 217 | export function parseTokenAccount(data: Buffer): AccountInfo { 218 | const accountInfo = AccountLayout.decode(data); 219 | accountInfo.mint = new PublicKey(accountInfo.mint); 220 | accountInfo.owner = new PublicKey(accountInfo.owner); 221 | accountInfo.amount = u64.fromBuffer(accountInfo.amount); 222 | 223 | if (accountInfo.delegateOption === 0) { 224 | accountInfo.delegate = null; 225 | // eslint-disable-next-line new-cap 226 | accountInfo.delegatedAmount = new u64(0); 227 | } else { 228 | accountInfo.delegate = new PublicKey(accountInfo.delegate); 229 | accountInfo.delegatedAmount = u64.fromBuffer(accountInfo.delegatedAmount); 230 | } 231 | 232 | accountInfo.isInitialized = accountInfo.state !== 0; 233 | accountInfo.isFrozen = accountInfo.state === 2; 234 | 235 | if (accountInfo.isNativeOption === 1) { 236 | accountInfo.rentExemptReserve = u64.fromBuffer(accountInfo.isNative); 237 | accountInfo.isNative = true; 238 | } else { 239 | accountInfo.rentExemptReserve = null; 240 | accountInfo.isNative = false; 241 | } 242 | 243 | if (accountInfo.closeAuthorityOption === 0) { 244 | accountInfo.closeAuthority = null; 245 | } else { 246 | accountInfo.closeAuthority = new PublicKey(accountInfo.closeAuthority); 247 | } 248 | 249 | return accountInfo; 250 | } 251 | 252 | export function sleep(ms: number): Promise { 253 | return new Promise(resolve => setTimeout(resolve, ms)); 254 | } 255 | 256 | export type ProgramAccount = { 257 | publicKey: PublicKey; 258 | account: T; 259 | }; 260 | -------------------------------------------------------------------------------- /packages/common/src/provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Account, 4 | PublicKey, 5 | Transaction, 6 | TransactionSignature, 7 | ConfirmOptions, 8 | sendAndConfirmRawTransaction, 9 | } from '@solana/web3.js'; 10 | import { simulateTransaction } from './simulate-transaction'; 11 | 12 | export class Provider { 13 | constructor( 14 | readonly connection: Connection, 15 | readonly wallet: Wallet, 16 | readonly opts: ConfirmOptions, 17 | ) {} 18 | 19 | static defaultOptions(): ConfirmOptions { 20 | return { 21 | preflightCommitment: 'recent', 22 | commitment: 'recent', 23 | }; 24 | } 25 | 26 | static local(url?: string, opts?: ConfirmOptions): Provider { 27 | opts = opts || Provider.defaultOptions(); 28 | const connection = new Connection( 29 | url || 'http://localhost:8899', 30 | opts.preflightCommitment, 31 | ); 32 | const wallet = NodeWallet.local(); 33 | return new Provider(connection, wallet, opts); 34 | } 35 | 36 | async send( 37 | tx: Transaction, 38 | signers?: Array, 39 | opts?: ConfirmOptions, 40 | ): Promise { 41 | if (signers === undefined) { 42 | signers = []; 43 | } 44 | if (opts === undefined) { 45 | opts = this.opts; 46 | } 47 | 48 | const signerKps = signers.filter(s => s !== undefined) as Array; 49 | const signerPubkeys = [this.wallet.publicKey].concat( 50 | signerKps.map(s => s.publicKey), 51 | ); 52 | 53 | tx.setSigners(...signerPubkeys); 54 | tx.recentBlockhash = ( 55 | await this.connection.getRecentBlockhash(opts.preflightCommitment) 56 | ).blockhash; 57 | 58 | await this.wallet.signTransaction(tx); 59 | signerKps.forEach(kp => { 60 | tx.partialSign(kp); 61 | }); 62 | 63 | const rawTx = tx.serialize(); 64 | 65 | try { 66 | const txId = await sendAndConfirmRawTransaction( 67 | this.connection, 68 | rawTx, 69 | opts, 70 | ); 71 | 72 | return txId; 73 | } catch (err) { 74 | console.error('Transaction failed. Simulating for logs...'); 75 | const r = await simulateTransaction( 76 | this.connection, 77 | tx, 78 | opts.commitment ?? 'recent', 79 | ); 80 | console.error(r); 81 | throw err; 82 | } 83 | } 84 | 85 | async sendAll( 86 | reqs: Array, 87 | opts?: ConfirmOptions, 88 | ): Promise> { 89 | if (opts === undefined) { 90 | opts = this.opts; 91 | } 92 | const blockhash = await this.connection.getRecentBlockhash( 93 | opts.preflightCommitment, 94 | ); 95 | 96 | let txs = reqs.map(r => { 97 | let tx = r.tx; 98 | let signers = r.signers; 99 | 100 | if (signers === undefined) { 101 | signers = []; 102 | } 103 | 104 | const signerKps = signers.filter(s => s !== undefined) as Array; 105 | const signerPubkeys = [this.wallet.publicKey].concat( 106 | signerKps.map(s => s.publicKey), 107 | ); 108 | 109 | tx.setSigners(...signerPubkeys); 110 | tx.recentBlockhash = blockhash.blockhash; 111 | signerKps.forEach(kp => { 112 | tx.partialSign(kp); 113 | }); 114 | 115 | return tx; 116 | }); 117 | 118 | const signedTxs = await this.wallet.signAllTransactions(txs); 119 | 120 | const sigs = []; 121 | 122 | for (let k = 0; k < txs.length; k += 1) { 123 | const tx = signedTxs[k]; 124 | const rawTx = tx.serialize(); 125 | try { 126 | sigs.push( 127 | await sendAndConfirmRawTransaction(this.connection, rawTx, opts), 128 | ); 129 | } catch (err) { 130 | console.error('Transaction failed. Simulating for logs...'); 131 | const r = await simulateTransaction( 132 | this.connection, 133 | tx, 134 | opts.commitment ?? 'recent', 135 | ); 136 | console.error(r); 137 | throw err; 138 | } 139 | } 140 | 141 | return sigs; 142 | } 143 | } 144 | 145 | export type SendTxRequest = { 146 | tx: Transaction; 147 | signers: Array; 148 | }; 149 | 150 | export interface Wallet { 151 | signTransaction(tx: Transaction): Promise; 152 | signAllTransactions(txs: Transaction[]): Promise; 153 | publicKey: PublicKey; 154 | } 155 | 156 | export class NodeWallet implements Wallet { 157 | constructor(readonly payer: Account) {} 158 | 159 | static local(): NodeWallet { 160 | const payer = new Account( 161 | Buffer.from( 162 | JSON.parse( 163 | require('fs').readFileSync( 164 | require('os').homedir() + '/.config/solana/id.json', 165 | { 166 | encoding: 'utf-8', 167 | }, 168 | ), 169 | ), 170 | ), 171 | ); 172 | return new NodeWallet(payer); 173 | } 174 | 175 | async signTransaction(tx: Transaction): Promise { 176 | tx.partialSign(this.payer); 177 | return tx; 178 | } 179 | 180 | async signAllTransactions(txs: Transaction[]): Promise { 181 | return txs.map(t => { 182 | t.partialSign(this.payer); 183 | return t; 184 | }); 185 | } 186 | 187 | get publicKey(): PublicKey { 188 | return this.payer.publicKey; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /packages/common/src/simulate-transaction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Commitment, 3 | Connection, 4 | RpcResponseAndContext, 5 | SimulatedTransactionResponse, 6 | Transaction, 7 | } from '@solana/web3.js'; 8 | 9 | /** Copy of Connection.simulateTransaction that takes a commitment parameter. */ 10 | export async function simulateTransaction( 11 | connection: Connection, 12 | transaction: Transaction, 13 | commitment: Commitment, 14 | ): Promise> { 15 | // @ts-ignore 16 | transaction.recentBlockhash = await connection._recentBlockhash( 17 | // @ts-ignore 18 | connection._disableBlockhashCaching, 19 | ); 20 | 21 | const signData = transaction.serializeMessage(); 22 | // @ts-ignore 23 | const wireTransaction = transaction._serialize(signData); 24 | const encodedTransaction = wireTransaction.toString('base64'); 25 | const config: any = { encoding: 'base64', commitment }; 26 | const args = [encodedTransaction, config]; 27 | 28 | // @ts-ignore 29 | const res = await connection._rpcRequest('simulateTransaction', args); 30 | if (res.error) { 31 | throw new Error('failed to simulate transaction: ' + res.error.message); 32 | } 33 | return res.result; 34 | } 35 | -------------------------------------------------------------------------------- /packages/common/src/token.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { ProgramAccount } from './'; 3 | import { AccountInfo as TokenAccount } from '@solana/spl-token'; 4 | import { TokenInstructions } from '@project-serum/serum'; 5 | import * as bs58 from 'bs58'; 6 | import * as BufferLayout from 'buffer-layout'; 7 | 8 | export async function getOwnedTokenAccounts( 9 | connection: Connection, 10 | publicKey: PublicKey, 11 | ): Promise[]> { 12 | let filters = getOwnedAccountsFilters(publicKey); 13 | // @ts-ignore 14 | let resp = await connection._rpcRequest('getProgramAccounts', [ 15 | TokenInstructions.TOKEN_PROGRAM_ID.toBase58(), 16 | { 17 | commitment: connection.commitment, 18 | filters, 19 | }, 20 | ]); 21 | if (resp.error) { 22 | throw new Error( 23 | 'failed to get token accounts owned by ' + 24 | publicKey.toBase58() + 25 | ': ' + 26 | resp.error.message, 27 | ); 28 | } 29 | return ( 30 | resp.result 31 | // @ts-ignore 32 | .map(({ pubkey, account: { data } }) => { 33 | data = bs58.decode(data); 34 | return { 35 | publicKey: new PublicKey(pubkey), 36 | account: parseTokenAccountData(data), 37 | }; 38 | }) 39 | ); 40 | } 41 | 42 | // todo: remove 43 | export const ACCOUNT_LAYOUT = BufferLayout.struct([ 44 | BufferLayout.blob(32, 'mint'), 45 | BufferLayout.blob(32, 'owner'), 46 | BufferLayout.nu64('amount'), 47 | BufferLayout.blob(93), 48 | ]); 49 | export const MINT_LAYOUT = BufferLayout.struct([ 50 | BufferLayout.blob(44), 51 | BufferLayout.u8('decimals'), 52 | BufferLayout.blob(37), 53 | ]); 54 | 55 | export function parseTokenAccountData(data: any) { 56 | // @ts-ignore 57 | let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data); 58 | return { 59 | mint: new PublicKey(mint), 60 | owner: new PublicKey(owner), 61 | amount, 62 | }; 63 | } 64 | 65 | // @ts-ignore 66 | export function parseMintData(data) { 67 | // @ts-ignore 68 | let { decimals } = MINT_LAYOUT.decode(data); 69 | return { decimals }; 70 | } 71 | 72 | // @ts-ignore 73 | export function getOwnedAccountsFilters(publicKey: PublicKey) { 74 | return [ 75 | { 76 | memcmp: { 77 | // @ts-ignore 78 | offset: ACCOUNT_LAYOUT.offsetOf('owner'), 79 | bytes: publicKey.toBase58(), 80 | }, 81 | }, 82 | { 83 | dataSize: ACCOUNT_LAYOUT.span, 84 | }, 85 | ]; 86 | } 87 | -------------------------------------------------------------------------------- /packages/common/test/example.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Example test suite', () => { 2 | it('Works', async () => { 3 | // no-op 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | 6 | "outDir": "./dist/lib", 7 | "rootDir": "./src", 8 | 9 | "composite": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | 14 | "strict": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "typeRoots": ["../../types/", "../../node_modules/@types", "../../types"] 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["src/**/*.test.ts", "**/node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/pool/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js 6 | -------------------------------------------------------------------------------- /packages/pool/README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@project-serum/pool)](https://www.npmjs.com/package/@project-serum/pool) 2 | [![Build Status](https://travis-ci.com/project-serum/serum-ts.svg?branch=master)](https://travis-ci.com/project-serum/serum-ts) 3 | [![Documentation](https://img.shields.io/badge/typedoc-documentation-blue)](https://project-serum.github.io/serum-ts/pool/modules/_index_.html) 4 | [![Discord Chat](https://img.shields.io/discord/739225212658122886?color=blueviolet)](https://discord.com/channels/739225212658122886) 5 | [![License](https://img.shields.io/github/license/project-serum/serum-dex?color=blue)](https://opensource.org/licenses/Apache-2.0) 6 | 7 | # Serum Pools JS Library 8 | 9 | JavaScript client library for interacting with Project Serum Pools. 10 | 11 | ## Installation 12 | 13 | Using npm: 14 | 15 | ``` 16 | npm install @solana/web3.js @project-serum/pool 17 | ``` 18 | 19 | Using yarn: 20 | 21 | ``` 22 | yarn add @solana/web3.js @project-serum/pool 23 | ``` 24 | 25 | ## Usage 26 | 27 | ### Load pool info 28 | 29 | Fetch and decode pool state: 30 | 31 | ```js 32 | import { Connection, PublicKey } from '@solana/web3.js'; 33 | import { loadPoolInfo, PoolTransactions } from '@project-serum/pool'; 34 | 35 | let connection = new Connection('...'); 36 | let poolAddress = new PublicKey('...'); // Address of the pool. 37 | 38 | let poolInfo = await loadPoolInfo(connection, poolAddress); 39 | console.log(poolInfo.state); 40 | ``` 41 | 42 | See [`loadPoolInfo()`](https://project-serum.github.io/serum-ts/pool/modules/_index_.html#loadpoolinfo) and [PoolState](https://project-serum.github.io/serum-ts/pool/interfaces/_index_.poolstate.html) for details. 43 | 44 | If you already have the pool state data and just need to decode it, you can 45 | call [`isPoolState()`](https://project-serum.github.io/serum-ts/pool/modules/_index_.html#ispoolstate) 46 | and [`decodePoolState()`](https://project-serum.github.io/serum-ts/pool/modules/_index_.html#decodepoolstate) 47 | directly. 48 | 49 | ```js 50 | import { decodePoolState } from '@project-serum/pool'; 51 | 52 | // Pool state account data, e.g. from Connection.getAccountInfo or Connection.onAccountChange 53 | let data = new Buffer('...'); 54 | 55 | let poolState = decodePoolState(data); 56 | console.log(poolState); 57 | ``` 58 | 59 | See [`PoolState`](https://project-serum.github.io/serum-ts/pool/interfaces/_index_.poolstate.html) 60 | for details on what the pool state contains. 61 | 62 | ### Get pool basket 63 | 64 | Use [`getPoolBasket()`](https://project-serum.github.io/serum-ts/pool/modules/_index_.html#getpoolbasket) 65 | to fetch the current pool basket (the quantity of each token needed to create N pool tokens 66 | or the quantity of each token received for redeeming N pool tokens). 67 | 68 | ```js 69 | import { Connection, PublicKey } from '@solana/web3.js'; 70 | import { loadPoolInfo, getPoolBasket } from '@project-serum/pool'; 71 | import BN from 'bn.js'; 72 | 73 | let connection = new Connection('...'); 74 | let poolAddress = new PublicKey('...'); // Address of the pool. 75 | 76 | let poolInfo = await loadPoolInfo(connection, poolAddress); 77 | let basket = await getPoolBasket( 78 | connection, 79 | poolInfo, 80 | { create: new BN(100) }, 81 | // Arbitrary SOL address, can be anything as long as it has nonzero SOL 82 | // and is not a program-owned address. 83 | new PublicKey('...'), 84 | ); 85 | 86 | console.log(basket); 87 | ``` 88 | 89 | ### Create pool tokens 90 | 91 | Send a transaction to create pool tokens: 92 | 93 | ```js 94 | import { Account, Connection, PublicKey } from '@solana/web3.js'; 95 | import { loadPoolInfo, PoolTransactions } from '@project-serum/pool'; 96 | import BN from 'bn.js'; 97 | 98 | let connection = new Connection('...'); 99 | let poolAddress = new PublicKey('...'); // Address of the pool. 100 | let payer = new Account('...'); // Account to pay for solana fees. 101 | 102 | let poolInfo = await loadPoolInfo(connection, poolAddress); 103 | let { transaction, signers } = PoolTransactions.execute( 104 | poolInfo, 105 | { 106 | // Number of tokens to create. 107 | create: new BN(100), 108 | }, 109 | { 110 | // Spl-token account to send the created tokens. 111 | poolTokenAccount: new PublicKey('...'), 112 | // Spl-token accounts to pull funds from. 113 | assetAccounts: [new PublicKey('...'), new Public('...')], 114 | // Owner of poolTokenAccount and assetAccounts. 115 | owner: payer.publicKey, 116 | }, 117 | // Expected creation cost. 118 | [new BN(10), new BN(10)], 119 | ); 120 | await connection.sendTransaction(transaction, [payer, ...signers]); 121 | ``` 122 | 123 | See [`PoolTransactions.execute`](https://project-serum.github.io/serum-ts/pool/classes/_index_.pooltransactions.html#execute) for details. 124 | 125 | ### Redeem pool tokens 126 | 127 | Send a transaction to redeem pool tokens: 128 | 129 | ```js 130 | import { Account, Connection, PublicKey } from '@solana/web3.js'; 131 | import { loadPoolInfo, PoolTransactions } from '@project-serum/pool'; 132 | import BN from 'bn.js'; 133 | 134 | let connection = new Connection('...'); 135 | let poolAddress = new PublicKey('...'); // Address of the pool. 136 | let payer = new Account('...'); // Account to pay for solana fees. 137 | 138 | let poolInfo = await loadPoolInfo(connection, poolAddress); 139 | let { transaction, signers } = PoolTransactions.execute( 140 | poolInfo, 141 | { 142 | // Number of tokens to redeem. 143 | redeem: new BN(100), 144 | }, 145 | { 146 | // Spl-token account to pull the pool tokens to redeem from. 147 | poolTokenAccount: new PublicKey('...'), 148 | // Spl-token accounts to send the redemption proceeds. 149 | assetAccounts: [new PublicKey('...'), new Public('...')], 150 | // Owner of poolTokenAccount and assetAccounts. 151 | owner: payer.publicKey, 152 | }, 153 | // Expected redemption proceeds. 154 | [new BN(10), new BN(10)], 155 | ); 156 | await connection.sendTransaction(transaction, [payer, ...signers]); 157 | ``` 158 | 159 | See [`PoolTransactions.execute`](https://project-serum.github.io/serum-ts/pool/classes/_index_.pooltransactions.html#execute) for details. 160 | 161 | ## API Reference 162 | 163 | [API Reference](https://project-serum.github.io/serum-ts/pool/modules/_index_.html) 164 | -------------------------------------------------------------------------------- /packages/pool/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/default', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/pool/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-serum/pool", 3 | "version": "0.2.3", 4 | "description": "Library for interacting with the serum pools", 5 | "repository": "project-serum/serum-ts", 6 | "main": "dist/lib/index.js", 7 | "source": "src/index.js", 8 | "types": "dist/lib/index.d.ts", 9 | "exports": { 10 | ".": "./dist/lib/index.js" 11 | }, 12 | "license": "Apache-2.0", 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "engines": { 17 | "node": ">=10" 18 | }, 19 | "scripts": { 20 | "build": "tsc", 21 | "start": "tsc --watch", 22 | "clean": "rm -rf dist", 23 | "prepare": "run-s clean build", 24 | "docs": "typedoc --out ../../docs/pool --mode library --composite false --rootDir src src/index.ts src/*.d.ts", 25 | "test": "run-s test:build test:unit test:lint", 26 | "test:build": "run-s build", 27 | "test:lint": "eslint src", 28 | "test:unit": "jest", 29 | "test:watch": "jest --watch" 30 | }, 31 | "dependencies": { 32 | "@project-serum/associated-token": "^0.1.1", 33 | "@project-serum/borsh": "^0.0.1-beta.0", 34 | "@project-serum/token": "^0.0.1-alpha.2", 35 | "bn.js": "^5.1.2", 36 | "buffer-layout": "^1.2.0" 37 | }, 38 | "peerDependencies": { 39 | "@solana/web3.js": "^0.90.0" 40 | }, 41 | "devDependencies": { 42 | "@solana/web3.js": "^0.90.0", 43 | "@types/bn.js": "^4.11.6", 44 | "@types/jest": "^26.0.9", 45 | "@typescript-eslint/eslint-plugin": "^4.6.0", 46 | "@typescript-eslint/parser": "^4.6.0", 47 | "babel-eslint": "^10.0.3", 48 | "cross-env": "^7.0.2", 49 | "eslint": "^7.12.1", 50 | "eslint-config-prettier": "^6.15.0", 51 | "jest": "^26.4.0", 52 | "npm-run-all": "^4.1.5", 53 | "ts-jest": "^26.4.3", 54 | "typedoc": "0.17.0-3", 55 | "typescript": "^4.0.5" 56 | }, 57 | "files": [ 58 | "dist" 59 | ], 60 | "browserslist": [ 61 | ">0.2%", 62 | "not dead", 63 | "not op_mini all", 64 | "maintained node versions" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /packages/pool/src/admin-controlled-pool.ts: -------------------------------------------------------------------------------- 1 | import { struct, u32 } from 'buffer-layout'; 2 | import { Layout, rustEnum, tagged, u64 } from '@project-serum/borsh'; 3 | import { 4 | AccountMeta, 5 | PublicKey, 6 | TransactionInstruction, 7 | } from '@solana/web3.js'; 8 | import BN from 'bn.js'; 9 | import { PoolInfo } from './instructions'; 10 | import { TOKEN_PROGRAM_ID } from '@project-serum/token'; 11 | 12 | export type AdminRequest = 13 | | { pause: any } 14 | | { unpause: any } 15 | | { approveDelegate: { amount: BN } } 16 | | { addAsset: any } 17 | | { removeAsset: any } 18 | | { updateFee: { feeRate: number } } 19 | | { updateAdmin: any }; 20 | 21 | export const ADMIN_INSTRUCTION_TAG = new BN('31e6452361a17878', 'hex'); 22 | 23 | export const AdminRequest: Layout = tagged( 24 | ADMIN_INSTRUCTION_TAG, 25 | rustEnum([ 26 | struct([], 'pause'), 27 | struct([], 'unpause'), 28 | struct([u64('amount')], 'approveDelegate'), 29 | struct([], 'addAsset'), 30 | struct([], 'removeAsset'), 31 | struct([u32('feeRate')], 'updateFee'), 32 | struct([], 'updateAdmin'), 33 | ]), 34 | ); 35 | 36 | function encodeAdminRequest(request: AdminRequest): Buffer { 37 | const buffer = Buffer.alloc(1000); 38 | const len = AdminRequest.encode(request, buffer); 39 | return buffer.slice(0, len); 40 | } 41 | 42 | function makeAdminInstruction( 43 | pool: PoolInfo, 44 | request: AdminRequest, 45 | keys?: Array, 46 | ): TransactionInstruction { 47 | if (!pool.state.adminKey) { 48 | throw new Error('Pool does not have admin'); 49 | } 50 | return new TransactionInstruction({ 51 | keys: [ 52 | { pubkey: pool.address, isSigner: false, isWritable: true }, 53 | { pubkey: pool.state.adminKey, isSigner: true, isWritable: false }, 54 | ...(keys ?? []), 55 | ], 56 | programId: pool.program, 57 | data: encodeAdminRequest(request), 58 | }); 59 | } 60 | 61 | /** Instructions for interacting with the example admin-controlled pool. */ 62 | export class AdminControlledPoolInstructions { 63 | /** Pauses creations and redemptions for the pool. */ 64 | static pause(pool: PoolInfo): TransactionInstruction { 65 | return makeAdminInstruction(pool, { pause: {} }); 66 | } 67 | /** 68 | * Resumes creations and redemptions for the pool. 69 | * 70 | * Pool assets must not have any outstanding delegates. 71 | */ 72 | static unpause(pool: PoolInfo): TransactionInstruction { 73 | return makeAdminInstruction( 74 | pool, 75 | { unpause: {} }, 76 | pool.state.assets.map(asset => ({ 77 | pubkey: asset.vaultAddress, 78 | isSigner: false, 79 | isWritable: false, 80 | })), 81 | ); 82 | } 83 | 84 | /** Approves an account to spend tokens on behalf of the pool. */ 85 | static approveDelegate( 86 | pool: PoolInfo, 87 | vault: PublicKey, 88 | delegate: PublicKey, 89 | amount: BN, 90 | ): TransactionInstruction { 91 | return makeAdminInstruction(pool, { approveDelegate: { amount } }, [ 92 | { pubkey: vault, isSigner: false, isWritable: true }, 93 | { pubkey: delegate, isSigner: false, isWritable: false }, 94 | { pubkey: pool.state.vaultSigner, isSigner: false, isWritable: false }, 95 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, 96 | ]); 97 | } 98 | 99 | /** Adds a new asset to the pool. */ 100 | static addAsset(pool: PoolInfo, vault: PublicKey): TransactionInstruction { 101 | return makeAdminInstruction(pool, { addAsset: {} }, [ 102 | { pubkey: vault, isSigner: false, isWritable: false }, 103 | ]); 104 | } 105 | 106 | /** 107 | * Removes an asset from the pool. 108 | * 109 | * The pool must not currently own any tokens of the asset to be removed. 110 | */ 111 | static removeAsset(pool: PoolInfo, vault: PublicKey): TransactionInstruction { 112 | return makeAdminInstruction(pool, { removeAsset: {} }, [ 113 | { pubkey: vault, isSigner: false, isWritable: false }, 114 | ]); 115 | } 116 | 117 | /** Modifies the fee rate for the pool. */ 118 | static updateFee(pool: PoolInfo, feeRate: number): TransactionInstruction { 119 | return makeAdminInstruction(pool, { updateFee: { feeRate } }); 120 | } 121 | 122 | /** Transfers admin permission for the pool to a new account. */ 123 | static updateAdmin( 124 | pool: PoolInfo, 125 | newAdmin: PublicKey, 126 | ): TransactionInstruction { 127 | return makeAdminInstruction(pool, { updateAdmin: {} }, [ 128 | { pubkey: newAdmin, isSigner: true, isWritable: false }, 129 | ]); 130 | } 131 | } 132 | 133 | export const ADMIN_CONTROLLED_POOL_TAG = new BN('4a3ab7f76f93f94e', 'hex'); 134 | 135 | export function isAdminControlledPool(pool: PoolInfo): boolean { 136 | return pool.state.customState 137 | .slice(0, 8) 138 | .equals(ADMIN_CONTROLLED_POOL_TAG.toArrayLike(Buffer, 'le', 8)); 139 | } 140 | -------------------------------------------------------------------------------- /packages/pool/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { Basket, decodePoolState, isPoolState, PoolAction } from './schema'; 3 | import { PoolInfo } from './instructions'; 4 | import { simulateTransaction } from './simulate-transaction'; 5 | import { PoolTransactions } from './transactions'; 6 | 7 | export * from './schema'; 8 | export { PoolInstructions, PoolInfo, UserInfo } from './instructions'; 9 | export { 10 | PoolTransactions, 11 | TransactionAndSigners, 12 | SimplePoolParams, 13 | } from './transactions'; 14 | export { 15 | AdminControlledPoolInstructions, 16 | isAdminControlledPool, 17 | } from './admin-controlled-pool'; 18 | 19 | /** 20 | * Load and decode pool state. 21 | * 22 | * Throws an error if the pool is not found or invalid. 23 | * 24 | * @param connection Solana connection to use to fetch the pool state. 25 | * @param address Pool state account address. 26 | */ 27 | export async function loadPoolInfo( 28 | connection: Connection, 29 | address: PublicKey, 30 | ): Promise { 31 | const accountInfo = await connection.getAccountInfo(address); 32 | if (accountInfo === null) { 33 | throw new Error('Pool does not exist'); 34 | } 35 | if (!isPoolState(accountInfo.data)) { 36 | throw new Error('Address is not a valid pool'); 37 | } 38 | return { 39 | address, 40 | state: decodePoolState(accountInfo.data), 41 | program: accountInfo.owner, 42 | }; 43 | } 44 | 45 | /** 46 | * Fetch the basket for a creation or redemption. 47 | * 48 | * For creations, the basket is the quantity of each asset that needs to be paid 49 | * to the pool to create the given quantity of pool tokens. 50 | * 51 | * For redemptions, the basket is the quantity of each asset that is received 52 | * from the pool in return for redeeming the given quantity of pool tokens. 53 | * 54 | * @param connection Connection to use to fetch data. 55 | * @param pool Pool to interact with. 56 | * @param action Creation, redemption, or swap. 57 | * @param payer Arbitrary Solana address. Must hold nonzero SOL and not be owned 58 | * by a program. 59 | */ 60 | export async function getPoolBasket( 61 | connection: Connection, 62 | pool: PoolInfo, 63 | action: PoolAction, 64 | payer: PublicKey = new PublicKey( 65 | 'H6WR1VVoiwWz1GMSBALwUNNazvE7UhAZEjaYTTSwvV8D', 66 | ), 67 | ): Promise { 68 | const { transaction } = PoolTransactions.getBasket(pool, action, payer); 69 | const { value } = await simulateTransaction( 70 | connection, 71 | transaction, 72 | connection.commitment ?? 'single', 73 | ); 74 | if (value.err) { 75 | console.warn('Program logs:', value.logs); 76 | throw new Error('Failed to get pool basket: ' + JSON.stringify(value.err)); 77 | } 78 | if (value.logs) { 79 | for (let i = value.logs.length - 1; i >= 0; --i) { 80 | if (value.logs[i].startsWith('Program log: ')) { 81 | const data = Buffer.from( 82 | value.logs[i].slice('Program log: '.length), 83 | 'base64', 84 | ); 85 | return Basket.decode(data); 86 | } 87 | } 88 | } 89 | throw new Error('Failed to find pool basket in logs'); 90 | } 91 | -------------------------------------------------------------------------------- /packages/pool/src/instructions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountMeta, 3 | PublicKey, 4 | SYSVAR_RENT_PUBKEY, 5 | TransactionInstruction, 6 | } from '@solana/web3.js'; 7 | import { encodePoolRequest, PoolAction, PoolState } from './schema'; 8 | import { TOKEN_PROGRAM_ID } from '@project-serum/token'; 9 | 10 | export interface PoolInfo { 11 | address: PublicKey; 12 | state: PoolState; 13 | program: PublicKey; 14 | } 15 | 16 | export interface UserInfo { 17 | /** spl-token account for the {@link PoolState.poolTokenMint pool token}. */ 18 | poolTokenAccount: PublicKey; 19 | /** 20 | * spl-token account for each of the {@link PoolState.assets assets} in the 21 | * pool. 22 | */ 23 | assetAccounts: PublicKey[]; 24 | /** 25 | * Owner or delegate of the token accounts. 26 | */ 27 | owner: PublicKey; 28 | /** 29 | * Optional spl-token account to which referral fees should be sent. 30 | */ 31 | referrer?: PublicKey; 32 | } 33 | 34 | export const RETBUF_PROGRAM_ID = new PublicKey( 35 | // TODO: switch to shmem4EWT2sPdVGvTZCzXXRAURL9G5vpPxNwSeKhHUL once that exists on mainnet 36 | 'BYVBQ71CYArTNbEpDnsPCjcoWkJL9181xvj52kfyFFHg', 37 | ); 38 | 39 | export const LQD_FEE_OWNER_ADDRESS = new PublicKey( 40 | '3LTvJCPiPSMjX1kBJ6ZfEhn4G2hM46aJ1yEZsk8i12TK', 41 | ); 42 | 43 | /** 44 | * Low-level API for constructing and encoding pool instructions. 45 | * 46 | * For a higher-level API that handles initializing accounts and approving token 47 | * transfers, use {@link PoolTransactions}. 48 | */ 49 | export class PoolInstructions { 50 | /** 51 | * Instruction to initialize a pool. 52 | * 53 | * @param poolProgram Program ID of the pool program. 54 | * @param poolAccount Newly-created account to hold the pool state. Must be 55 | * owned by the pool program. 56 | * @param poolTokenMint spl-token mint address for the pool token. 57 | * @param poolName User-friendly name for the pool. 58 | * @param vaults spl-token account for each of the assets in the pool. 59 | * @param vaultSigner Mint authority for `poolTokenMint` and owner of 60 | * `poolTokenMint`. 61 | * @param vaultSignerNonce Nonce used to generate `vaultSigner`. 62 | * @param lqdFeeAccount Pool token spl-token account owned by the LQD fee owner. 63 | * @param initializerFeeAccount Pool token spl-token account owned by the pool initializer. 64 | * @param feeRate Fee rate for creations/redemptions times 10^6. 65 | * @param additionalAccounts Any custom pool-specific accounts needed to 66 | * initialize the pool. 67 | * @param customData Any custom pool-specific data needed to initialize the pool 68 | */ 69 | static initialize( 70 | poolProgram: PublicKey, 71 | poolAccount: PublicKey, 72 | poolTokenMint: PublicKey, 73 | poolName: string, 74 | vaults: PublicKey[], 75 | vaultSigner: PublicKey, 76 | vaultSignerNonce: number, 77 | lqdFeeAccount: PublicKey, 78 | initializerFeeAccount: PublicKey, 79 | feeRate: number, 80 | additionalAccounts?: AccountMeta[], 81 | customData?: Buffer, 82 | ): TransactionInstruction { 83 | return new TransactionInstruction({ 84 | keys: [ 85 | { pubkey: poolAccount, isSigner: false, isWritable: true }, 86 | { pubkey: poolTokenMint, isSigner: false, isWritable: true }, 87 | ...vaults.map(vaultAddress => ({ 88 | pubkey: vaultAddress, 89 | isSigner: false, 90 | isWritable: true, 91 | })), 92 | { pubkey: vaultSigner, isSigner: false, isWritable: false }, 93 | { pubkey: lqdFeeAccount, isSigner: false, isWritable: false }, 94 | { pubkey: initializerFeeAccount, isSigner: false, isWritable: false }, 95 | { 96 | pubkey: SYSVAR_RENT_PUBKEY, 97 | isSigner: false, 98 | isWritable: false, 99 | }, 100 | ...(additionalAccounts ?? []), 101 | ], 102 | programId: poolProgram, 103 | data: encodePoolRequest({ 104 | initialize: { 105 | vaultSignerNonce, 106 | assetsLength: vaults.length, 107 | poolName, 108 | feeRate, 109 | customData: customData ?? Buffer.alloc(0), 110 | }, 111 | }), 112 | }); 113 | } 114 | 115 | /** 116 | * Instruction to get the expected basket for a {@link PoolAction}. 117 | * 118 | * For creations, the basket is the quantity of each asset 119 | * that need to be sent to the pool to process the creation. For redemptions 120 | * and swaps, the basket is the quantity of each asset that will be 121 | * transferred from the pool to the user after the redemption or swap. 122 | * 123 | * Negative quantities will cause tokens to be transferred in the opposite 124 | * direction. 125 | * 126 | * @param pool 127 | * @param action Creation, redemption, or swap. 128 | * @param retbufAccount Account to which the basket will be written. Must be 129 | * owned by the retbuf program. 130 | */ 131 | static getBasket( 132 | pool: PoolInfo, 133 | action: PoolAction, 134 | retbufAccount: PublicKey, 135 | ): TransactionInstruction { 136 | return new TransactionInstruction({ 137 | keys: [ 138 | { pubkey: pool.address, isSigner: false, isWritable: false }, 139 | { 140 | pubkey: pool.state.poolTokenMint, 141 | isSigner: false, 142 | isWritable: false, 143 | }, 144 | ...pool.state.assets.map(assetInfo => ({ 145 | pubkey: assetInfo.vaultAddress, 146 | isSigner: false, 147 | isWritable: false, 148 | })), 149 | { pubkey: pool.state.vaultSigner, isSigner: false, isWritable: false }, 150 | { pubkey: retbufAccount, isSigner: false, isWritable: true }, 151 | { pubkey: RETBUF_PROGRAM_ID, isSigner: false, isWritable: false }, 152 | ...pool.state.accountParams.map(paramInfo => ({ 153 | pubkey: paramInfo.address, 154 | isSigner: false, 155 | isWritable: false, 156 | })), 157 | ], 158 | programId: pool.program, 159 | data: encodePoolRequest({ getBasket: action }), 160 | }); 161 | } 162 | 163 | /** 164 | * Instruction to execute a creation, redemption, or swap. 165 | * 166 | * @param pool 167 | * @param action Creation, redemption, or swap. 168 | * @param user Token accounts to pull funds from or send funds to. 169 | */ 170 | static execute( 171 | pool: PoolInfo, 172 | action: PoolAction, 173 | user: UserInfo, 174 | ): TransactionInstruction { 175 | return new TransactionInstruction({ 176 | keys: [ 177 | { pubkey: pool.address, isSigner: false, isWritable: true }, 178 | { pubkey: pool.state.poolTokenMint, isSigner: false, isWritable: true }, 179 | ...pool.state.assets.map(assetInfo => ({ 180 | pubkey: assetInfo.vaultAddress, 181 | isSigner: false, 182 | isWritable: true, 183 | })), 184 | { pubkey: pool.state.vaultSigner, isSigner: false, isWritable: false }, 185 | { pubkey: user.poolTokenAccount, isSigner: false, isWritable: true }, 186 | ...user.assetAccounts.map(address => ({ 187 | pubkey: address, 188 | isSigner: false, 189 | isWritable: true, 190 | })), 191 | { pubkey: user.owner, isSigner: true, isWritable: false }, 192 | { 193 | pubkey: pool.state.lqdFeeVault, 194 | isSigner: false, 195 | isWritable: true, 196 | }, 197 | { 198 | pubkey: pool.state.initializerFeeVault, 199 | isSigner: false, 200 | isWritable: true, 201 | }, 202 | { 203 | pubkey: user.referrer ?? pool.state.lqdFeeVault, 204 | isSigner: false, 205 | isWritable: true, 206 | }, 207 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, 208 | ...pool.state.accountParams.map(paramInfo => ({ 209 | pubkey: paramInfo.address, 210 | isSigner: false, 211 | isWritable: paramInfo.writable, 212 | })), 213 | ], 214 | programId: pool.program, 215 | data: encodePoolRequest({ execute: action }), 216 | }); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /packages/pool/src/schema.test.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { Basket, PoolRequest, PoolState } from '@project-serum/pool'; 3 | import BN from 'bn.js'; 4 | 5 | describe('PoolState', () => { 6 | it('round trips', () => { 7 | const b = Buffer.alloc(1000); 8 | const state = { 9 | poolTokenMint: new PublicKey( 10 | 'HsQhg1k93vEA326SXxnGj1sZrdupG7rj5T6g5cMgk1ed', 11 | ), 12 | assets: [ 13 | { 14 | mint: new PublicKey('BLvBqiEt4C2zn5dZmGLwBocFSbhbsZadV7Hd9oaSHNqj'), 15 | vaultAddress: new PublicKey( 16 | 'GSUcLyZfwK5MUC8hkjGEAZLCkvjru3jk6nK29N1ciKrF', 17 | ), 18 | }, 19 | ], 20 | vaultSigner: new PublicKey( 21 | '4pvTcVX3K4mc1dLs1vQ8Sq3Z2NWVGXsjpfJ6tmvnj533', 22 | ), 23 | vaultSignerNonce: 0, 24 | accountParams: [ 25 | { 26 | address: new PublicKey( 27 | 'HvLPFuTLa8RShBiQfT17tKntA69pNoV7o2XXSyRnaf4e', 28 | ), 29 | writable: true, 30 | }, 31 | { 32 | address: new PublicKey( 33 | '6J6QfKjcQojqGZwRNoQ4do48UnV5KqNM7M5Rb2e6eDWx', 34 | ), 35 | writable: false, 36 | }, 37 | ], 38 | name: 'Test', 39 | lqdFeeVault: new PublicKey( 40 | '77AkdYcu3DjtzmJYRVnFymfcdaqJjjFhRnKyyDPjQHYF', 41 | ), 42 | initializerFeeVault: new PublicKey( 43 | '4HfeDayMfG9GtaJ9ZBsuDFWHYH3eYiiTXRpFL8uyiYHi', 44 | ), 45 | feeRate: 150, 46 | adminKey: null, 47 | customState: Buffer.alloc(10), 48 | }; 49 | 50 | const len = PoolState.encode(state, b); 51 | expect(PoolState.getSpan(b)).toBe(len); 52 | expect(PoolState.decode(b.slice(0, len))).toEqual(state); 53 | }); 54 | }); 55 | 56 | describe('PoolRequest', () => { 57 | it('round trips', () => { 58 | const b = Buffer.alloc(1000); 59 | const request = { 60 | getBasket: { 61 | swap: { 62 | quantities: [new BN(12)], 63 | }, 64 | }, 65 | }; 66 | 67 | const len = PoolRequest.encode(request, b); 68 | expect(PoolRequest.getSpan(b)).toBe(len); 69 | expect(PoolRequest.decode(b.slice(0, len))).toEqual(request); 70 | }); 71 | }); 72 | 73 | describe('Basket', () => { 74 | it('round trips', () => { 75 | const b = Buffer.alloc(1000); 76 | const basket = { quantities: [new BN(123), new BN(-123)] }; 77 | 78 | const len = Basket.encode(basket, b); 79 | expect(Basket.getSpan(b)).toBe(len); 80 | expect(Basket.decode(b.slice(0, len))).toEqual(basket); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /packages/pool/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { struct, u32, u8 } from 'buffer-layout'; 2 | import { 3 | bool, 4 | i64, 5 | Layout, 6 | option, 7 | publicKey, 8 | rustEnum, 9 | str, 10 | tagged, 11 | u64, 12 | vec, 13 | vecU8, 14 | } from '@project-serum/borsh'; 15 | import { PublicKey } from '@solana/web3.js'; 16 | import BN from 'bn.js'; 17 | 18 | /** 19 | * Decoded pool state. 20 | */ 21 | export interface PoolState { 22 | /** Token mint address for the pool token. */ 23 | poolTokenMint: PublicKey; 24 | /** Assets in the pool. */ 25 | assets: AssetInfo[]; 26 | /** Owner of the assets in the pool. */ 27 | vaultSigner: PublicKey; 28 | /** Nonce used to generate `vaultSigner`; only used internally by the pool program. */ 29 | vaultSignerNonce: number; 30 | /** Accounts that must be included in requests to create or redeem tokens. */ 31 | accountParams: ParamDesc[]; 32 | /** User-friendly name for the pool. */ 33 | name: string; 34 | /** Token vault address for fees collected by the pool for LQD. */ 35 | lqdFeeVault: PublicKey; 36 | /** Token vault address for fees collected by the pool for the pool initializer. */ 37 | initializerFeeVault: PublicKey; 38 | /** Fee on creations/redemptions, times 10^6. */ 39 | feeRate: number; 40 | /** Admin for the pool. Not used by default but may have pool-specific semantics. */ 41 | adminKey: PublicKey | null; 42 | /** Custom pool-specific state. */ 43 | customState: Buffer; 44 | } 45 | 46 | /** Describes one of the assets in the pool. */ 47 | export interface AssetInfo { 48 | /** Token mint address for the asset. */ 49 | mint: PublicKey; 50 | /** Token vault address for the asset. */ 51 | vaultAddress: PublicKey; 52 | } 53 | 54 | export interface ParamDesc { 55 | address: PublicKey; 56 | writable: boolean; 57 | } 58 | 59 | export type PoolRequest = 60 | | { initialize: InitializePoolRequest } 61 | | { getBasket: PoolAction } 62 | | { execute: PoolAction }; 63 | 64 | export interface InitializePoolRequest { 65 | vaultSignerNonce: number; 66 | assetsLength: number; 67 | poolName: string; 68 | feeRate: number; 69 | customData: Buffer; 70 | } 71 | 72 | export type PoolAction = { create: BN } | { redeem: BN } | { swap: Basket }; 73 | 74 | export interface Basket { 75 | quantities: BN[]; 76 | } 77 | 78 | export const AssetInfo: Layout = struct([ 79 | publicKey('mint'), 80 | publicKey('vaultAddress'), 81 | ]); 82 | 83 | export const ParamDesc: Layout = struct([ 84 | publicKey('address'), 85 | bool('writable'), 86 | ]); 87 | 88 | export const POOL_STATE_TAG = new BN('16a7874c7fb2301b', 'hex'); 89 | 90 | export const PoolState: Layout = tagged( 91 | POOL_STATE_TAG, 92 | struct([ 93 | publicKey('poolTokenMint'), 94 | vec(AssetInfo, 'assets'), 95 | publicKey('vaultSigner'), 96 | u8('vaultSignerNonce'), 97 | vec(ParamDesc, 'accountParams'), 98 | str('name'), 99 | publicKey('lqdFeeVault'), 100 | publicKey('initializerFeeVault'), 101 | u32('feeRate'), 102 | option(publicKey(), 'adminKey'), 103 | vecU8('customState'), 104 | ]), 105 | ); 106 | 107 | export const Basket: Layout = struct([vec(i64(), 'quantities')]); 108 | 109 | export const PoolAction: Layout = rustEnum([ 110 | u64('create'), 111 | u64('redeem'), 112 | Basket.replicate('swap'), 113 | ]); 114 | 115 | export const InitializePoolRequest: Layout = struct([ 116 | u8('vaultSignerNonce'), 117 | u8('assetsLength'), 118 | str('poolName'), 119 | u32('feeRate'), 120 | vecU8('customData'), 121 | ]); 122 | 123 | export const POOL_REQUEST_TAG = new BN('220a6cbdcd1cc4cf', 'hex'); 124 | 125 | export const PoolRequest: Layout = tagged( 126 | POOL_REQUEST_TAG, 127 | rustEnum([ 128 | InitializePoolRequest.replicate('initialize'), 129 | PoolAction.replicate('getBasket'), 130 | PoolAction.replicate('execute'), 131 | ]), 132 | ); 133 | 134 | export function isPoolState(data: Buffer): boolean { 135 | return data.slice(0, 8).equals(POOL_STATE_TAG.toBuffer('le')); 136 | } 137 | 138 | export function decodePoolState(data: Buffer): PoolState { 139 | return PoolState.decode(data); 140 | } 141 | 142 | export function encodePoolState(state: PoolState): Buffer { 143 | const buffer = Buffer.alloc(1000); 144 | const len = PoolState.encode(state, buffer); 145 | return buffer.slice(0, len); 146 | } 147 | 148 | export function encodePoolRequest(poolRequest: PoolRequest): Buffer { 149 | const buffer = Buffer.alloc(1000); 150 | const len = PoolRequest.encode(poolRequest, buffer); 151 | return buffer.slice(0, len); 152 | } 153 | 154 | export function decodePoolRequest(data: Buffer): PoolRequest { 155 | return PoolRequest.decode(data); 156 | } 157 | -------------------------------------------------------------------------------- /packages/pool/src/scripts/create-pool-and-do-stuff-with-it.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Account, 3 | Connection, 4 | PublicKey, 5 | SystemProgram, 6 | Transaction, 7 | } from '@solana/web3.js'; 8 | import { TOKEN_PROGRAM_ID, TokenInstructions } from '@project-serum/token'; 9 | import { promisify } from 'util'; 10 | import { homedir } from 'os'; 11 | import { readFile } from 'fs'; 12 | import BN from 'bn.js'; 13 | import { PoolTransactions } from '../transactions'; 14 | import { getPoolBasket, loadPoolInfo, PoolInfo, UserInfo } from '../index'; 15 | import { getAssociatedTokenAddress } from '@project-serum/associated-token'; 16 | 17 | const POOL_PROGRAM_ID = new PublicKey( 18 | 'ERvQUuLLY89DcwiUYemUgogdt2TFh7CG7cNW1fEFzeMJ', 19 | ); 20 | 21 | async function doStuff() { 22 | const connection = new Connection('http://localhost:8899', 'recent'); 23 | const payer = new Account( 24 | Buffer.from( 25 | JSON.parse( 26 | await promisify(readFile)(homedir() + '/.config/solana/id.json', { 27 | encoding: 'utf-8', 28 | }), 29 | ), 30 | ), 31 | ); 32 | 33 | const [mint1, vault1] = await createMint(connection, payer); 34 | const [mint2, vault2] = await createMint(connection, payer); 35 | 36 | const [ 37 | poolAddress, 38 | transactions, 39 | ] = await PoolTransactions.initializeSimplePool({ 40 | connection, 41 | assetMints: [mint1, mint2], 42 | creator: payer.publicKey, 43 | creatorAssets: [vault1, vault2], 44 | initialAssetQuantities: [new BN(100), new BN(300)], 45 | poolStateSpace: 1000, 46 | programId: POOL_PROGRAM_ID, 47 | poolName: 'Test Pool', 48 | feeRate: 2500, 49 | }); 50 | console.log('Pool address:', poolAddress.toBase58()); 51 | for (const { transaction, signers } of transactions) { 52 | await sendAndConfirmTransaction(connection, transaction, [ 53 | payer, 54 | ...signers, 55 | ]); 56 | } 57 | 58 | const poolInfo = await loadPoolInfo(connection, poolAddress); 59 | console.log(poolInfo); 60 | const userInfo: UserInfo = { 61 | owner: payer.publicKey, 62 | poolTokenAccount: await getAssociatedTokenAddress( 63 | payer.publicKey, 64 | poolInfo.state.poolTokenMint, 65 | ), 66 | assetAccounts: [vault1, vault2], 67 | }; 68 | 69 | console.log( 70 | await getPoolBasket( 71 | connection, 72 | poolInfo, 73 | { create: new BN(1) }, 74 | payer.publicKey, 75 | ), 76 | ); 77 | console.log( 78 | await getPoolBasket( 79 | connection, 80 | poolInfo, 81 | { redeem: new BN(1) }, 82 | payer.publicKey, 83 | ), 84 | ); 85 | console.log( 86 | await getPoolBasket( 87 | connection, 88 | poolInfo, 89 | { create: new BN(1000000) }, 90 | payer.publicKey, 91 | ), 92 | ); 93 | console.log( 94 | await getPoolBasket( 95 | connection, 96 | poolInfo, 97 | { create: new BN(2000000) }, 98 | payer.publicKey, 99 | ), 100 | ); 101 | console.log( 102 | await getPoolBasket( 103 | connection, 104 | poolInfo, 105 | { redeem: new BN(2000000) }, 106 | payer.publicKey, 107 | ), 108 | ); 109 | 110 | { 111 | const { transaction, signers } = PoolTransactions.execute( 112 | poolInfo, 113 | { create: new BN(1000000) }, 114 | userInfo, 115 | { 116 | quantities: [new BN(100), new BN(300)], 117 | }, 118 | ); 119 | await sendAndConfirmTransaction(connection, transaction, [ 120 | payer, 121 | ...signers, 122 | ]); 123 | } 124 | 125 | console.log( 126 | await getPoolBasket( 127 | connection, 128 | poolInfo, 129 | { create: new BN(1000000) }, 130 | payer.publicKey, 131 | ), 132 | ); 133 | 134 | { 135 | const { transaction, signers } = PoolTransactions.execute( 136 | poolInfo, 137 | { redeem: new BN(2000000 - poolInfo.state.feeRate) }, 138 | userInfo, 139 | { 140 | quantities: [new BN(200), new BN(600)], 141 | }, 142 | ); 143 | await sendAndConfirmTransaction(connection, transaction, [ 144 | payer, 145 | ...signers, 146 | ]); 147 | } 148 | 149 | console.log( 150 | await getPoolBasket( 151 | connection, 152 | poolInfo, 153 | { create: new BN(1000000) }, 154 | payer.publicKey, 155 | ), 156 | ); 157 | } 158 | 159 | async function createMint(connection: Connection, payer: Account) { 160 | const mint = new Account(); 161 | const vault = new Account(); 162 | const txn = new Transaction(); 163 | txn.add( 164 | SystemProgram.createAccount({ 165 | fromPubkey: payer.publicKey, 166 | newAccountPubkey: mint.publicKey, 167 | space: 82, 168 | lamports: await connection.getMinimumBalanceForRentExemption(82), 169 | programId: TOKEN_PROGRAM_ID, 170 | }), 171 | TokenInstructions.initializeMint({ 172 | mint: mint.publicKey, 173 | decimals: 0, 174 | mintAuthority: payer.publicKey, 175 | }), 176 | SystemProgram.createAccount({ 177 | fromPubkey: payer.publicKey, 178 | newAccountPubkey: vault.publicKey, 179 | space: 165, 180 | lamports: await connection.getMinimumBalanceForRentExemption(165), 181 | programId: TOKEN_PROGRAM_ID, 182 | }), 183 | TokenInstructions.initializeAccount({ 184 | account: vault.publicKey, 185 | mint: mint.publicKey, 186 | owner: payer.publicKey, 187 | }), 188 | TokenInstructions.mintTo({ 189 | mint: mint.publicKey, 190 | destination: vault.publicKey, 191 | amount: new BN(10000), 192 | mintAuthority: payer.publicKey, 193 | }), 194 | ); 195 | await sendAndConfirmTransaction(connection, txn, [payer, mint, vault]); 196 | return [mint.publicKey, vault.publicKey]; 197 | } 198 | 199 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 200 | async function createUserAccounts( 201 | connection: Connection, 202 | payer: Account, 203 | pool: PoolInfo, 204 | ): Promise { 205 | const poolTokenAccount = new Account(); 206 | const assetAccounts: Account[] = []; 207 | const lamports = await connection.getMinimumBalanceForRentExemption(165); 208 | const txn = new Transaction(); 209 | txn.add( 210 | SystemProgram.createAccount({ 211 | fromPubkey: payer.publicKey, 212 | newAccountPubkey: poolTokenAccount.publicKey, 213 | space: 165, 214 | lamports, 215 | programId: TOKEN_PROGRAM_ID, 216 | }), 217 | TokenInstructions.initializeAccount({ 218 | account: poolTokenAccount.publicKey, 219 | mint: pool.state.poolTokenMint, 220 | owner: payer.publicKey, 221 | }), 222 | ); 223 | pool.state.assets.forEach(({ mint }) => { 224 | const account = new Account(); 225 | assetAccounts.push(account); 226 | txn.add( 227 | SystemProgram.createAccount({ 228 | fromPubkey: payer.publicKey, 229 | newAccountPubkey: account.publicKey, 230 | space: 165, 231 | lamports, 232 | programId: TOKEN_PROGRAM_ID, 233 | }), 234 | TokenInstructions.initializeAccount({ 235 | account: account.publicKey, 236 | mint, 237 | owner: payer.publicKey, 238 | }), 239 | ); 240 | }); 241 | txn.feePayer = payer.publicKey; 242 | await sendAndConfirmTransaction(connection, txn, [ 243 | payer, 244 | poolTokenAccount, 245 | ...assetAccounts, 246 | ]); 247 | return { 248 | owner: payer.publicKey, 249 | poolTokenAccount: poolTokenAccount.publicKey, 250 | assetAccounts: assetAccounts.map(account => account.publicKey), 251 | }; 252 | } 253 | 254 | async function sendAndConfirmTransaction( 255 | connection: Connection, 256 | transaction: Transaction, 257 | signers: Account[], 258 | ) { 259 | const txid = await connection.sendTransaction(transaction, signers, { 260 | preflightCommitment: 'recent', 261 | }); 262 | await connection.confirmTransaction(txid, 'recent'); 263 | return txid; 264 | } 265 | 266 | doStuff().catch(e => console.error(e)); 267 | -------------------------------------------------------------------------------- /packages/pool/src/simulate-transaction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Commitment, 3 | Connection, 4 | RpcResponseAndContext, 5 | SimulatedTransactionResponse, 6 | Transaction, 7 | } from '@solana/web3.js'; 8 | 9 | /** Copy of Connection.simulateTransaction that takes a commitment parameter. */ 10 | export async function simulateTransaction( 11 | connection: Connection, 12 | transaction: Transaction, 13 | commitment: Commitment, 14 | ): Promise> { 15 | // @ts-ignore 16 | transaction.recentBlockhash = await connection._recentBlockhash( 17 | // @ts-ignore 18 | connection._disableBlockhashCaching, 19 | ); 20 | 21 | const signData = transaction.serializeMessage(); 22 | // @ts-ignore 23 | const wireTransaction = transaction._serialize(signData); 24 | const encodedTransaction = wireTransaction.toString('base64'); 25 | const config: any = { encoding: 'base64', commitment }; 26 | const args = [encodedTransaction, config]; 27 | 28 | // @ts-ignore 29 | const res = await connection._rpcRequest('simulateTransaction', args); 30 | if (res.error) { 31 | throw new Error('failed to simulate transaction: ' + res.error.message); 32 | } 33 | return res.result; 34 | } 35 | -------------------------------------------------------------------------------- /packages/pool/src/transactions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PoolInfo, 3 | PoolInstructions, 4 | RETBUF_PROGRAM_ID, 5 | LQD_FEE_OWNER_ADDRESS, 6 | UserInfo, 7 | } from './instructions'; 8 | import { 9 | Account, 10 | AccountMeta, 11 | Connection, 12 | PublicKey, 13 | SystemProgram, 14 | Transaction, 15 | } from '@solana/web3.js'; 16 | import { 17 | TokenInstructions, 18 | TOKEN_PROGRAM_ID, 19 | WRAPPED_SOL_MINT, 20 | } from '@project-serum/token'; 21 | import { Basket, PoolAction } from './schema'; 22 | import BN from 'bn.js'; 23 | import { 24 | createAssociatedTokenAccount, 25 | getAssociatedTokenAddress, 26 | } from '@project-serum/associated-token'; 27 | 28 | export interface TransactionAndSigners { 29 | transaction: Transaction; 30 | /** 31 | * Auto-generated accounts that need to sign the transaction. Note that this does not include 32 | * the user (fee payer and spl-token owner) account. 33 | */ 34 | signers: Account[]; 35 | } 36 | 37 | export interface SimplePoolParams { 38 | /** Connection to use to fetch fees. */ 39 | connection: Connection; 40 | 41 | /** Program ID of the pool program. */ 42 | programId: PublicKey; 43 | 44 | /** Size of pool state account, in bytes. */ 45 | poolStateSpace: number; 46 | 47 | /** User-friendly name for the pool. */ 48 | poolName: string; 49 | 50 | /** 51 | * Number of decimals for the to-be-created pool token. 52 | * 53 | * Defaults to 6. 54 | */ 55 | poolMintDecimals?: number; 56 | /** Mint addresses for the tokens in the pool. */ 57 | assetMints: PublicKey[]; 58 | 59 | /** 60 | * Initial quantity of outstanding tokens, sent to the pool creator. 61 | * 62 | * Defaults to `10 ** poolMintDecimals`. 63 | */ 64 | initialPoolMintSupply?: BN; 65 | /** Initial quantities of assets in the pool, sent from the pool creator. */ 66 | initialAssetQuantities: BN[]; 67 | 68 | /** 69 | * Owner for the spl-token accounts from which the initial pool assets are 70 | * taken and to which the newly created pool tokens are sent. 71 | */ 72 | creator: PublicKey; 73 | /** Spl-token accounts from which the initial pool assets are taken. */ 74 | creatorAssets: PublicKey[]; 75 | 76 | /** Fee rate for creations and redemptions, times 10 ** 6. */ 77 | feeRate?: number; 78 | 79 | /** Any additional accounts needed to initalize the pool. */ 80 | additionalAccounts?: AccountMeta[]; 81 | } 82 | 83 | /** 84 | * High-level API for constructing transactions to interact with pools. 85 | * 86 | * For a lower-level API, see {@link PoolInstructions}. 87 | */ 88 | export class PoolTransactions { 89 | /** 90 | * Transaction to initialize a simple pool. 91 | * 92 | * This will: 93 | * - initialize a new pool token mint 94 | * - initialize a pool token account for the creator and mint some tokens to it 95 | * - initialize vault accounts for each of the pool assets 96 | * - transfer assets from the pool creator to the vault accounts 97 | * - initialize the pool 98 | */ 99 | static async initializeSimplePool( 100 | params: SimplePoolParams, 101 | ): Promise<[PublicKey, TransactionAndSigners[]]> { 102 | const { 103 | connection, 104 | programId, 105 | poolStateSpace, 106 | poolMintDecimals = 6, 107 | poolName, 108 | assetMints, 109 | initialPoolMintSupply = new BN('1' + '0'.repeat(poolMintDecimals)), 110 | initialAssetQuantities, 111 | creator, 112 | creatorAssets, 113 | feeRate = 2500, 114 | additionalAccounts = [], 115 | } = params; 116 | if (assetMints.length !== initialAssetQuantities.length) { 117 | throw new Error( 118 | 'assetMints and initialAssetQuantities must have the same length', 119 | ); 120 | } 121 | if (assetMints.length !== creatorAssets.length) { 122 | throw new Error('assetMints and creatorAssets must have the same length'); 123 | } 124 | 125 | const poolStateAccount = new Account(); 126 | const [vaultSigner, vaultSignerNonce] = await PublicKey.findProgramAddress( 127 | [poolStateAccount.publicKey.toBuffer()], 128 | programId, 129 | ); 130 | const poolTokenMint = new Account(); 131 | const creatorPoolTokenAddress = await getAssociatedTokenAddress( 132 | creator, 133 | poolTokenMint.publicKey, 134 | ); 135 | const vaultAddresses = await Promise.all( 136 | assetMints.map(mint => getAssociatedTokenAddress(vaultSigner, mint)), 137 | ); 138 | const lqdFeeAddress = await getAssociatedTokenAddress( 139 | LQD_FEE_OWNER_ADDRESS, 140 | poolTokenMint.publicKey, 141 | ); 142 | 143 | // Split into two transactions to stay under the size limit. 144 | // Ideally all instructions that transfer tokens happen in the second transaction, 145 | // so they get reverted if the pool creation fails. 146 | const setup = { 147 | transaction: new Transaction(), 148 | signers: [poolTokenMint], 149 | }; 150 | const finalize = { 151 | transaction: new Transaction(), 152 | signers: [poolStateAccount], 153 | }; 154 | 155 | const mintAccountSpace = 82; 156 | const mintAccountLamports = await connection.getMinimumBalanceForRentExemption( 157 | mintAccountSpace, 158 | ); 159 | 160 | // Initialize pool token. 161 | setup.transaction.add( 162 | SystemProgram.createAccount({ 163 | fromPubkey: creator, 164 | newAccountPubkey: poolTokenMint.publicKey, 165 | space: mintAccountSpace, 166 | lamports: mintAccountLamports, 167 | programId: TOKEN_PROGRAM_ID, 168 | }), 169 | TokenInstructions.initializeMint({ 170 | mint: poolTokenMint.publicKey, 171 | decimals: poolMintDecimals, 172 | mintAuthority: creator, 173 | }), 174 | await createAssociatedTokenAccount( 175 | creator, 176 | creator, 177 | poolTokenMint.publicKey, 178 | ), 179 | await createAssociatedTokenAccount( 180 | creator, 181 | LQD_FEE_OWNER_ADDRESS, 182 | poolTokenMint.publicKey, 183 | ), 184 | ); 185 | finalize.transaction.add( 186 | TokenInstructions.mintTo({ 187 | mint: poolTokenMint.publicKey, 188 | destination: creatorPoolTokenAddress, 189 | amount: initialPoolMintSupply, 190 | mintAuthority: creator, 191 | }), 192 | TokenInstructions.setAuthority({ 193 | target: poolTokenMint.publicKey, 194 | currentAuthority: creator, 195 | newAuthority: vaultSigner, 196 | authorityType: 0, // AuthorityType::MintTokens 197 | }), 198 | ); 199 | 200 | // Initialize vault accounts. 201 | await Promise.all( 202 | assetMints.map(async (mint, index) => { 203 | const vault = vaultAddresses[index]; 204 | setup.transaction.add( 205 | await createAssociatedTokenAccount(creator, vaultSigner, mint), 206 | ); 207 | finalize.transaction.add( 208 | TokenInstructions.transfer({ 209 | source: creatorAssets[index], 210 | destination: vault, 211 | amount: initialAssetQuantities[index], 212 | owner: creator, 213 | }), 214 | ); 215 | }), 216 | ); 217 | 218 | // Initialize pool account. 219 | finalize.transaction.add( 220 | SystemProgram.createAccount({ 221 | fromPubkey: creator, 222 | newAccountPubkey: poolStateAccount.publicKey, 223 | space: poolStateSpace, 224 | lamports: await connection.getMinimumBalanceForRentExemption( 225 | poolStateSpace, 226 | ), 227 | programId: programId, 228 | }), 229 | PoolInstructions.initialize( 230 | programId, 231 | poolStateAccount.publicKey, 232 | poolTokenMint.publicKey, 233 | poolName, 234 | vaultAddresses, 235 | vaultSigner, 236 | vaultSignerNonce, 237 | lqdFeeAddress, 238 | creatorPoolTokenAddress, 239 | feeRate, 240 | additionalAccounts, 241 | ), 242 | ); 243 | 244 | return [poolStateAccount.publicKey, [setup, finalize]]; 245 | } 246 | 247 | /** 248 | * Transaction to get a pool basket, for use with simulateTransaction. 249 | * 250 | * This is a wrapper around {@link PoolInstructions.getBasket} that handles 251 | * initializing the retbuf account. 252 | * 253 | * @param pool Pool to interact with. 254 | * @param action Creation, redemption, or swap. 255 | * @param payer Payer for fees. Must have nonzero SOL but will not be charged 256 | * if the transaction is only simulated. 257 | */ 258 | static getBasket( 259 | pool: PoolInfo, 260 | action: PoolAction, 261 | payer: PublicKey, 262 | ): TransactionAndSigners { 263 | const transaction = new Transaction(); 264 | const retbufAccount = new Account(); 265 | transaction.add( 266 | SystemProgram.createAccount({ 267 | fromPubkey: payer, 268 | newAccountPubkey: retbufAccount.publicKey, 269 | lamports: 0, 270 | space: 1024, 271 | programId: RETBUF_PROGRAM_ID, 272 | }), 273 | ); 274 | transaction.add( 275 | PoolInstructions.getBasket(pool, action, retbufAccount.publicKey), 276 | ); 277 | transaction.feePayer = payer; 278 | return { transaction, signers: [retbufAccount] }; 279 | } 280 | 281 | /** 282 | * Transaction to execute a creation, redemption, or swap. 283 | * 284 | * This is a wrapper around {@link PoolInstructions.execute} that handles 285 | * token delegation. 286 | * 287 | * The transaction will create a temporary account, delegate tokens to it, 288 | * and use it in place of the actual owner, to limit the quantity of tokens 289 | * that the pool can pull from the user's accounts. 290 | * 291 | * @param pool Pool to interact with. 292 | * @param action Creation, redemption, or swap. 293 | * @param user Token accounts to pull funds from or send funds to. 294 | * @param expectedBasket Expected basket. Used to determine the quantity of 295 | * tokens to approve. 296 | */ 297 | static execute( 298 | pool: PoolInfo, 299 | action: PoolAction, 300 | user: UserInfo, 301 | expectedBasket: Basket, 302 | ): TransactionAndSigners { 303 | if (expectedBasket.quantities.length !== pool.state.assets.length) { 304 | throw new Error( 305 | 'expectedBasket must have the same number of components as the pool', 306 | ); 307 | } 308 | const transaction = new Transaction(); 309 | const delegate = new Account(); 310 | const signers = [delegate]; 311 | user = { ...user, assetAccounts: user.assetAccounts.slice() }; 312 | let wrappedSolAccount: Account | null = null; 313 | 314 | function approveDelegate(amount: BN, index: number, approveZero = false) { 315 | if ( 316 | user.assetAccounts[index].equals(user.owner) && 317 | pool.state.assets[index].mint.equals(WRAPPED_SOL_MINT) 318 | ) { 319 | wrappedSolAccount = new Account(); 320 | signers.push(wrappedSolAccount); 321 | transaction.add( 322 | SystemProgram.createAccount({ 323 | fromPubkey: user.owner, 324 | newAccountPubkey: wrappedSolAccount.publicKey, 325 | lamports: amount.toNumber() + 2.04e6, 326 | space: 165, 327 | programId: TOKEN_PROGRAM_ID, 328 | }), 329 | TokenInstructions.initializeAccount({ 330 | account: wrappedSolAccount.publicKey, 331 | mint: WRAPPED_SOL_MINT, 332 | owner: delegate.publicKey, 333 | }), 334 | ); 335 | user.assetAccounts[index] = wrappedSolAccount.publicKey; 336 | } else if (amount.gtn(0) || approveZero) { 337 | transaction.add( 338 | TokenInstructions.approve({ 339 | owner: user.owner, 340 | source: user.assetAccounts[index], 341 | delegate: delegate.publicKey, 342 | amount, 343 | }), 344 | ); 345 | } 346 | } 347 | 348 | if ('create' in action) { 349 | expectedBasket.quantities.forEach((amount, index) => { 350 | approveDelegate(amount, index, true); 351 | }); 352 | } else if ('redeem' in action) { 353 | transaction.add( 354 | TokenInstructions.approve({ 355 | owner: user.owner, 356 | source: user.poolTokenAccount, 357 | delegate: delegate.publicKey, 358 | amount: action.redeem, 359 | }), 360 | ); 361 | expectedBasket.quantities.forEach((amount, index) => { 362 | if (amount.isNeg()) { 363 | approveDelegate(amount.abs(), index); 364 | } else { 365 | approveDelegate(new BN(0), index); 366 | } 367 | }); 368 | } else if ('swap' in action) { 369 | action.swap.quantities.forEach((amount, index) => { 370 | approveDelegate(amount, index); 371 | }); 372 | } 373 | transaction.add( 374 | PoolInstructions.execute(pool, action, { 375 | ...user, 376 | owner: delegate.publicKey, 377 | }), 378 | ); 379 | if (wrappedSolAccount) { 380 | transaction.add( 381 | TokenInstructions.closeAccount({ 382 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 383 | source: wrappedSolAccount!.publicKey, 384 | destination: user.owner, 385 | owner: delegate.publicKey, 386 | }), 387 | ); 388 | } 389 | return { transaction, signers }; 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /packages/pool/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | 6 | "outDir": "./dist/lib", 7 | "rootDir": "./src", 8 | 9 | "composite": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | 14 | "strict": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "typeRoots": ["../../types/", "../../node_modules/@types", "../../packages/borsh/dist/lib"] 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["src/**/*.test.ts", "**/node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/serum/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /packages/serum/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier", 8 | "prettier/@typescript-eslint" 9 | ], 10 | "env": { 11 | "node": true, 12 | "jest": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "no-constant-condition": ["error", { "checkLoops": false }], 20 | "@typescript-eslint/explicit-module-boundary-types": "off", 21 | "@typescript-eslint/ban-ts-comment": "off", 22 | "@typescript-eslint/no-explicit-any": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/serum/README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@project-serum/serum)](https://www.npmjs.com/package/@project-serum/serum) 2 | [![Build Status](https://travis-ci.com/project-serum/serum-js.svg?branch=master)](https://travis-ci.com/project-serum/serum-js) 3 | 4 | # Serum JS Client Library 5 | 6 | JavaScript client library for interacting with the Project Serum DEX. 7 | 8 | ## Installation 9 | 10 | Using npm: 11 | 12 | ``` 13 | npm install @solana/web3.js @project-serum/serum 14 | ``` 15 | 16 | Using yarn: 17 | 18 | ``` 19 | yarn add @solana/web3.js @project-serum/serum 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```js 25 | import { Account, Connection, PublicKey } from '@solana/web3.js'; 26 | import { Market } from '@project-serum/serum'; 27 | 28 | let connection = new Connection('https://testnet.solana.com'); 29 | let marketAddress = new PublicKey('...'); 30 | let programAddress = new PublicKey("..."); 31 | let market = await Market.load(connection, marketAddress, {}, programAddress); 32 | 33 | // Fetching orderbooks 34 | let bids = await market.loadBids(connection); 35 | let asks = await market.loadAsks(connection); 36 | // L2 orderbook data 37 | for (let [price, size] of bids.getL2(20)) { 38 | console.log(price, size); 39 | } 40 | // Full orderbook data 41 | for (let order of asks) { 42 | console.log( 43 | order.orderId, 44 | order.price, 45 | order.size, 46 | order.side, // 'buy' or 'sell' 47 | ); 48 | } 49 | 50 | // Placing orders 51 | let owner = new Account('...'); 52 | let payer = new PublicKey('...'); // spl-token account 53 | await market.placeOrder(connection, { 54 | owner, 55 | payer, 56 | side: 'buy', // 'buy' or 'sell' 57 | price: 123.45, 58 | size: 17.0, 59 | orderType: 'limit', // 'limit', 'ioc', 'postOnly' 60 | }); 61 | 62 | // Retrieving open orders by owner 63 | let myOrders = await market.loadOrdersForOwner(connection, owner.publicKey); 64 | 65 | // Cancelling orders 66 | for (let order of myOrders) { 67 | await market.cancelOrder(connection, owner, order); 68 | } 69 | 70 | // Retrieving fills 71 | for (let fill of await market.loadFills(connection)) { 72 | console.log(fill.orderId, fill.price, fill.size, fill.side); 73 | } 74 | 75 | // Settle funds 76 | for (let openOrders of await market.findOpenOrdersAccountsForOwner( 77 | connection, 78 | owner.publicKey, 79 | )) { 80 | if (openOrders.baseTokenFree > 0 || openOrders.quoteTokenFree > 0) { 81 | // spl-token accounts to which to send the proceeds from trades 82 | let baseTokenAccount = new PublicKey('...'); 83 | let quoteTokenAccount = new PublicKey('...'); 84 | 85 | await market.settleFunds( 86 | connection, 87 | owner, 88 | openOrders, 89 | baseTokenAccount, 90 | quoteTokenAccount, 91 | ); 92 | } 93 | } 94 | ``` 95 | -------------------------------------------------------------------------------- /packages/serum/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/js-with-ts', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/serum/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-serum/serum", 3 | "version": "0.13.65", 4 | "description": "Library for interacting with the serum dex", 5 | "license": "MIT", 6 | "repository": "project-serum/serum-ts", 7 | "main": "lib/index.js", 8 | "types": "lib/index.d.ts", 9 | "engines": { 10 | "node": ">=10" 11 | }, 12 | "scripts": { 13 | "build": "tsc", 14 | "start": "tsc --watch", 15 | "clean": "rm -rf lib", 16 | "prepare": "run-s clean build", 17 | "shell": "node -e \"$(< shell)\" -i --experimental-repl-await", 18 | "test": "run-s test:unit test:lint test:build", 19 | "test:build": "run-s build", 20 | "test:lint": "eslint src", 21 | "test:unit": "jest", 22 | "test:watch": "jest --watch" 23 | }, 24 | "devDependencies": { 25 | "@tsconfig/node12": "^1.0.7", 26 | "@types/bn.js": "^4.11.6", 27 | "@types/jest": "^26.0.9", 28 | "@typescript-eslint/eslint-plugin": "^4.6.0", 29 | "@typescript-eslint/parser": "^4.6.0", 30 | "babel-eslint": "^10.0.3", 31 | "cross-env": "^7.0.2", 32 | "eslint": "^7.6.0", 33 | "eslint-config-prettier": "^6.11.0", 34 | "jest": "^26.4.0", 35 | "npm-run-all": "^4.1.5", 36 | "prettier": "^2.0.5", 37 | "ts-jest": "^26.2.0", 38 | "typescript": "^4.0.5" 39 | }, 40 | "files": [ 41 | "lib" 42 | ], 43 | "prettier": { 44 | "singleQuote": true, 45 | "trailingComma": "all" 46 | }, 47 | "dependencies": { 48 | "@project-serum/anchor": "^0.11.1", 49 | "@solana/spl-token": "^0.1.6", 50 | "@solana/web3.js": "^1.21.0", 51 | "bn.js": "^5.1.2", 52 | "buffer-layout": "^1.2.0" 53 | }, 54 | "browserslist": [ 55 | ">0.2%", 56 | "not dead", 57 | "not op_mini all", 58 | "maintained node versions" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /packages/serum/shell: -------------------------------------------------------------------------------- 1 | const lib = require('./lib/index'); 2 | const solana = require('@solana/web3.js'); 3 | const Market = lib.Market; 4 | const Orderbook = lib.Orderbook; 5 | const OpenOrders = lib.OpenOrders; 6 | const DexInstructions = lib.DexInstructions; 7 | const DEX_PROGRAM_ID = lib.DEX_PROGRAM_ID; 8 | const decodeEventQueue = lib.decodeEventQueue; 9 | const decodeRequestQueue = lib.decodeRequestQueue; 10 | const TokenInstructions = lib.TokenInstructions; 11 | const getLayoutVersion = lib.getLayoutVersion; 12 | -------------------------------------------------------------------------------- /packages/serum/src/error.ts: -------------------------------------------------------------------------------- 1 | import { Transaction, SystemProgram } from '@solana/web3.js'; 2 | import { PROGRAM_LAYOUT_VERSIONS } from './tokens_and_markets'; 3 | import { TOKEN_PROGRAM_ID } from './token-instructions'; 4 | 5 | export enum DexError { 6 | InvalidMarketFlags = 0, 7 | InvalidAskFlags, 8 | InvalidBidFlags, 9 | InvalidQueueLength, 10 | OwnerAccountNotProvided, 11 | 12 | ConsumeEventsQueueFailure, 13 | WrongCoinVault, 14 | WrongPcVault, 15 | WrongCoinMint, 16 | WrongPcMint, 17 | 18 | CoinVaultProgramId = 10, 19 | PcVaultProgramId, 20 | CoinMintProgramId, 21 | PcMintProgramId, 22 | 23 | WrongCoinMintSize, 24 | WrongPcMintSize, 25 | WrongCoinVaultSize, 26 | WrongPcVaultSize, 27 | 28 | UninitializedVault, 29 | UninitializedMint, 30 | 31 | CoinMintUninitialized = 20, 32 | PcMintUninitialized, 33 | WrongMint, 34 | WrongVaultOwner, 35 | VaultHasDelegate, 36 | 37 | AlreadyInitialized, 38 | WrongAccountDataAlignment, 39 | WrongAccountDataPaddingLength, 40 | WrongAccountHeadPadding, 41 | WrongAccountTailPadding, 42 | 43 | RequestQueueEmpty = 30, 44 | EventQueueTooSmall, 45 | SlabTooSmall, 46 | BadVaultSignerNonce, 47 | InsufficientFunds, 48 | 49 | SplAccountProgramId, 50 | SplAccountLen, 51 | WrongFeeDiscountAccountOwner, 52 | WrongFeeDiscountMint, 53 | 54 | CoinPayerProgramId, 55 | PcPayerProgramId = 40, 56 | ClientIdNotFound, 57 | TooManyOpenOrders, 58 | 59 | FakeErrorSoWeDontChangeNumbers, 60 | BorrowError, 61 | 62 | WrongOrdersAccount, 63 | WrongBidsAccount, 64 | WrongAsksAccount, 65 | WrongRequestQueueAccount, 66 | WrongEventQueueAccount, 67 | 68 | RequestQueueFull = 50, 69 | EventQueueFull, 70 | MarketIsDisabled, 71 | WrongSigner, 72 | TransferFailed, 73 | ClientOrderIdIsZero, 74 | 75 | WrongRentSysvarAccount, 76 | RentNotProvided, 77 | OrdersNotRentExempt, 78 | OrderNotFound, 79 | OrderNotYours, 80 | 81 | WouldSelfTrade, 82 | 83 | Unknown = 1000, 84 | } 85 | 86 | export const KNOWN_PROGRAMS = { 87 | [TOKEN_PROGRAM_ID.toString()]: 'Token program', 88 | [SystemProgram.programId.toString()]: 'System program', 89 | }; 90 | 91 | type CustomError = { Custom: number }; 92 | type InstructionError = [number, CustomError]; 93 | 94 | export function parseInstructionErrorResponse( 95 | transaction: Transaction, 96 | errorResponse: InstructionError, 97 | ): { 98 | failedInstructionIndex: number; 99 | error: string; 100 | failedProgram: string; 101 | } { 102 | const [failedInstructionIndex, customError] = errorResponse; 103 | const failedInstruction = transaction.instructions[failedInstructionIndex]; 104 | let parsedError; 105 | if (failedInstruction.programId.toString() in PROGRAM_LAYOUT_VERSIONS) { 106 | parsedError = DexError[customError['Custom']]; 107 | } else if (failedInstruction.programId.toString() in KNOWN_PROGRAMS) { 108 | const program = KNOWN_PROGRAMS[failedInstruction.programId.toString()]; 109 | parsedError = `${program} error ${customError['Custom']}`; 110 | } else { 111 | parsedError = `Unknown program ${failedInstruction.programId.toString()} custom error: ${ 112 | customError['Custom'] 113 | }`; 114 | } 115 | return { 116 | failedInstructionIndex, 117 | error: parsedError, 118 | failedProgram: failedInstruction.programId.toString(), 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /packages/serum/src/fees.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { getLayoutVersion } from './tokens_and_markets'; 3 | 4 | export function supportsSrmFeeDiscounts(programId: PublicKey) { 5 | return getLayoutVersion(programId) > 1; 6 | } 7 | 8 | export function getFeeRates(feeTier: number): { taker: number; maker: number } { 9 | if (feeTier === 1) { 10 | // SRM2 11 | return { taker: 0.002, maker: -0.0003 }; 12 | } else if (feeTier === 2) { 13 | // SRM3 14 | return { taker: 0.0018, maker: -0.0003 }; 15 | } else if (feeTier === 3) { 16 | // SRM4 17 | return { taker: 0.0016, maker: -0.0003 }; 18 | } else if (feeTier === 4) { 19 | // SRM5 20 | return { taker: 0.0014, maker: -0.0003 }; 21 | } else if (feeTier === 5) { 22 | // SRM6 23 | return { taker: 0.0012, maker: -0.0003 }; 24 | } else if (feeTier === 6) { 25 | // MSRM 26 | return { taker: 0.001, maker: -0.0005 }; 27 | } 28 | // Base 29 | return { taker: 0.0022, maker: -0.0003 }; 30 | } 31 | 32 | export function getFeeTier(msrmBalance: number, srmBalance: number): number { 33 | if (msrmBalance >= 1) { 34 | return 6; 35 | } else if (srmBalance >= 1_000_000) { 36 | return 5; 37 | } else if (srmBalance >= 100_000) { 38 | return 4; 39 | } else if (srmBalance >= 10_000) { 40 | return 3; 41 | } else if (srmBalance >= 1_000) { 42 | return 2; 43 | } else if (srmBalance >= 100) { 44 | return 1; 45 | } else { 46 | return 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/serum/src/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | Market, 3 | Orderbook, 4 | OpenOrders, 5 | MARKET_STATE_LAYOUT_V3, 6 | MARKET_STATE_LAYOUT_V2, 7 | } from './market'; 8 | export { 9 | DexInstructions, 10 | decodeInstruction, 11 | decodeInstructionV2, 12 | SETTLE_FUNDS_BASE_WALLET_INDEX, 13 | SETTLE_FUNDS_QUOTE_WALLET_INDEX, 14 | NEW_ORDER_OPEN_ORDERS_INDEX, 15 | NEW_ORDER_OWNER_INDEX, 16 | NEW_ORDER_V3_OPEN_ORDERS_INDEX, 17 | NEW_ORDER_V3_OWNER_INDEX, 18 | } from './instructions'; 19 | export { getFeeTier, getFeeRates, supportsSrmFeeDiscounts } from './fees'; 20 | export { TOKEN_MINTS, MARKETS, getLayoutVersion } from './tokens_and_markets'; 21 | export { 22 | decodeEventQueue, 23 | decodeRequestQueue, 24 | REQUEST_QUEUE_LAYOUT, 25 | EVENT_QUEUE_LAYOUT, 26 | } from './queue'; 27 | export * as TokenInstructions from './token-instructions'; 28 | export * from './error'; 29 | export { MarketProxy, MarketProxyBuilder } from './market-proxy'; 30 | export { 31 | OpenOrdersPda, 32 | ReferralFees, 33 | PermissionedCrank, 34 | Logger, 35 | Middleware, 36 | } from './market-proxy/middleware'; 37 | -------------------------------------------------------------------------------- /packages/serum/src/instructions.test.js: -------------------------------------------------------------------------------- 1 | import { encodeInstruction } from './instructions'; 2 | import BN from 'bn.js'; 3 | 4 | describe('instruction', () => { 5 | it('encodes initialize market', () => { 6 | const b = encodeInstruction({ 7 | initializeMarket: { 8 | baseLotSize: new BN(10), 9 | quoteLotSize: new BN(100000), 10 | feeRateBps: 5, 11 | vaultSignerNonce: new BN(1), 12 | quoteDustThreshold: new BN(10), 13 | }, 14 | }); 15 | expect(b.toString('hex')).toEqual( 16 | '00000000000a00000000000000a086010000000000050001000000000000000a00000000000000', 17 | ); 18 | }); 19 | 20 | it('encodes new order', () => { 21 | const b = encodeInstruction({ 22 | newOrder: { 23 | side: 'sell', 24 | limitPrice: new BN(10), 25 | maxQuantity: new BN(5), 26 | orderType: 'postOnly', 27 | }, 28 | }); 29 | expect(b.toString('hex')).toEqual( 30 | '0001000000010000000a000000000000000500000000000000020000000000000000000000', 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/serum/src/layout.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { bits, Blob, Layout, u32, UInt } from 'buffer-layout'; 4 | import { PublicKey } from '@solana/web3.js'; 5 | import BN from 'bn.js'; 6 | 7 | class Zeros extends Blob { 8 | decode(b, offset) { 9 | const slice = super.decode(b, offset); 10 | if (!slice.every((v) => v === 0)) { 11 | throw new Error('nonzero padding bytes'); 12 | } 13 | return slice; 14 | } 15 | } 16 | 17 | export function zeros(length) { 18 | return new Zeros(length); 19 | } 20 | 21 | class PublicKeyLayout extends Blob { 22 | constructor(property) { 23 | super(32, property); 24 | } 25 | 26 | decode(b, offset) { 27 | return new PublicKey(super.decode(b, offset)); 28 | } 29 | 30 | encode(src, b, offset) { 31 | return super.encode(src.toBuffer(), b, offset); 32 | } 33 | } 34 | 35 | export function publicKeyLayout(property) { 36 | return new PublicKeyLayout(property); 37 | } 38 | 39 | class BNLayout extends Blob { 40 | decode(b, offset) { 41 | return new BN(super.decode(b, offset), 10, 'le'); 42 | } 43 | 44 | encode(src, b, offset) { 45 | return super.encode(src.toArrayLike(Buffer, 'le', this.span), b, offset); 46 | } 47 | } 48 | 49 | export function u64(property) { 50 | return new BNLayout(8, property); 51 | } 52 | 53 | export function i64(property) { 54 | return new BNLayout(8, property); 55 | } 56 | 57 | export function u128(property) { 58 | return new BNLayout(16, property); 59 | } 60 | 61 | export class WideBits extends Layout { 62 | constructor(property) { 63 | super(8, property); 64 | this._lower = bits(u32(), false); 65 | this._upper = bits(u32(), false); 66 | } 67 | 68 | addBoolean(property) { 69 | if (this._lower.fields.length < 32) { 70 | this._lower.addBoolean(property); 71 | } else { 72 | this._upper.addBoolean(property); 73 | } 74 | } 75 | 76 | decode(b, offset = 0) { 77 | const lowerDecoded = this._lower.decode(b, offset); 78 | const upperDecoded = this._upper.decode(b, offset + this._lower.span); 79 | return { ...lowerDecoded, ...upperDecoded }; 80 | } 81 | 82 | encode(src, b, offset = 0) { 83 | return ( 84 | this._lower.encode(src, b, offset) + 85 | this._upper.encode(src, b, offset + this._lower.span) 86 | ); 87 | } 88 | } 89 | 90 | export class VersionedLayout extends Layout { 91 | constructor(version, inner, property) { 92 | super(inner.span > 0 ? inner.span + 1 : inner.span, property); 93 | this.version = version; 94 | this.inner = inner; 95 | } 96 | 97 | decode(b, offset = 0) { 98 | // if (b.readUInt8(offset) !== this._version) { 99 | // throw new Error('invalid version'); 100 | // } 101 | return this.inner.decode(b, offset + 1); 102 | } 103 | 104 | encode(src, b, offset = 0) { 105 | b.writeUInt8(this.version, offset); 106 | return 1 + this.inner.encode(src, b, offset + 1); 107 | } 108 | 109 | getSpan(b, offset = 0) { 110 | return 1 + this.inner.getSpan(b, offset + 1); 111 | } 112 | } 113 | 114 | class EnumLayout extends UInt { 115 | constructor(values, span, property) { 116 | super(span, property); 117 | this.values = values; 118 | } 119 | 120 | encode(src, b, offset) { 121 | if (this.values[src] !== undefined) { 122 | return super.encode(this.values[src], b, offset); 123 | } 124 | throw new Error('Invalid ' + this.property); 125 | } 126 | 127 | decode(b, offset) { 128 | const decodedValue = super.decode(b, offset); 129 | const entry = Object.entries(this.values).find( 130 | ([, value]) => value === decodedValue, 131 | ); 132 | if (entry) { 133 | return entry[0]; 134 | } 135 | throw new Error('Invalid ' + this.property); 136 | } 137 | } 138 | 139 | export function sideLayout(property) { 140 | return new EnumLayout({ buy: 0, sell: 1 }, 4, property); 141 | } 142 | 143 | export function orderTypeLayout(property) { 144 | return new EnumLayout({ limit: 0, ioc: 1, postOnly: 2 }, 4, property); 145 | } 146 | 147 | export function selfTradeBehaviorLayout(property) { 148 | return new EnumLayout( 149 | { decrementTake: 0, cancelProvide: 1, abortTransaction: 2 }, 150 | 4, 151 | property, 152 | ); 153 | } 154 | 155 | const ACCOUNT_FLAGS_LAYOUT = new WideBits(); 156 | ACCOUNT_FLAGS_LAYOUT.addBoolean('initialized'); 157 | ACCOUNT_FLAGS_LAYOUT.addBoolean('market'); 158 | ACCOUNT_FLAGS_LAYOUT.addBoolean('openOrders'); 159 | ACCOUNT_FLAGS_LAYOUT.addBoolean('requestQueue'); 160 | ACCOUNT_FLAGS_LAYOUT.addBoolean('eventQueue'); 161 | ACCOUNT_FLAGS_LAYOUT.addBoolean('bids'); 162 | ACCOUNT_FLAGS_LAYOUT.addBoolean('asks'); 163 | 164 | export function accountFlagsLayout(property = 'accountFlags') { 165 | return ACCOUNT_FLAGS_LAYOUT.replicate(property); 166 | } 167 | 168 | export function setLayoutDecoder(layout, decoder) { 169 | const originalDecode = layout.decode; 170 | layout.decode = function decode(b, offset = 0) { 171 | return decoder(originalDecode.call(this, b, offset)); 172 | }; 173 | } 174 | 175 | export function setLayoutEncoder(layout, encoder) { 176 | const originalEncode = layout.encode; 177 | layout.encode = function encode(src, b, offset) { 178 | return originalEncode.call(this, encoder(src), b, offset); 179 | }; 180 | return layout; 181 | } 182 | -------------------------------------------------------------------------------- /packages/serum/src/market-proxy/index.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'; 3 | import { utils } from '@project-serum/anchor'; 4 | import { 5 | Market, 6 | MarketOptions, 7 | OrderParams, 8 | MARKET_STATE_LAYOUT_V3, 9 | Order, 10 | } from '../market'; 11 | import { DexInstructions } from '../instructions'; 12 | import { Middleware } from './middleware'; 13 | 14 | // MarketProxy provides an API for constructing transactions to an on-chain 15 | // DEX proxy, which relays all instructions to the orderbook. Minimally, this 16 | // requires two modifications for DEX instructions. 17 | // 18 | // 1. Transasctions are sent to the proxy program--not the DEX. 19 | // 2. The DEX program ID must be inserted as the first account in instructions 20 | // using the proxy relay, so that the proxy can use the account for CPI. 21 | // The program is responsible for removing this account before relaying to 22 | // the dex. 23 | // 24 | // Additionally, a middleware abstraction is provided so that one can configure 25 | // both the client and the smart contract with the ability to send and processs 26 | // arbitrary accounts and instruction data *in addition* to what the Serum DEX 27 | // expects. 28 | // 29 | // Similar to the layers of an onion, each middleware wraps a transaction 30 | // request with additional accounts and instruction data before sending it to 31 | // the program. Upon receiving the request, the program--with its own set of 32 | // middleware-- unwraps and processes each layer. The process ends with all 33 | // layers being unwrapped and the proxy relaying the transaction to the DEX. 34 | // 35 | // As a result, the order of the middleware matters and the client should 36 | // process middleware in the *reverse* order of the proxy smart contract. 37 | export class MarketProxy { 38 | // DEX market being proxied. 39 | get market(): Market { 40 | return this._market; 41 | } 42 | private _market: Market; 43 | 44 | // Instruction namespace. 45 | get instruction(): MarketProxyInstruction { 46 | return this._instruction; 47 | } 48 | private _instruction: MarketProxyInstruction; 49 | 50 | // Serum DEX program ID. 51 | get dexProgramId(): PublicKey { 52 | return this._market.programId; 53 | } 54 | 55 | // Proxy program ID. 56 | get proxyProgramId(): PublicKey { 57 | return this._instruction.proxyProgramId; 58 | } 59 | 60 | // Ctor. 61 | constructor(market: Market, instruction: MarketProxyInstruction) { 62 | this._market = market; 63 | this._instruction = instruction; 64 | } 65 | } 66 | 67 | // Instruction builder for the market proxy. 68 | export class MarketProxyInstruction { 69 | // Program ID of the permissioning proxy program. 70 | get proxyProgramId(): PublicKey { 71 | return this._proxyProgramId; 72 | } 73 | private _proxyProgramId: PublicKey; 74 | 75 | // Dex program ID. 76 | private _dexProgramId: PublicKey; 77 | 78 | // Underlying DEX market. 79 | private _market: Market; 80 | 81 | // Middlewares for processing the creation of transactions. 82 | private _middlewares: Middleware[]; 83 | 84 | constructor( 85 | proxyProgramId: PublicKey, 86 | dexProgramId: PublicKey, 87 | market: Market, 88 | middlewares: Middleware[], 89 | ) { 90 | this._proxyProgramId = proxyProgramId; 91 | this._dexProgramId = dexProgramId; 92 | this._market = market; 93 | this._middlewares = middlewares; 94 | } 95 | 96 | public newOrderV3(params: OrderParams): TransactionInstruction { 97 | const tradeIx = this._market.makeNewOrderV3Instruction({ 98 | ...params, 99 | programId: this._proxyProgramId, 100 | }); 101 | this._middlewares.forEach((mw) => mw.newOrderV3(tradeIx)); 102 | return this.proxy(tradeIx); 103 | } 104 | 105 | public initOpenOrders( 106 | owner: PublicKey, 107 | market: PublicKey, 108 | openOrders: PublicKey, 109 | marketAuthority: PublicKey, 110 | ): TransactionInstruction { 111 | const ix = DexInstructions.initOpenOrders({ 112 | market, 113 | openOrders, 114 | owner, 115 | programId: this._proxyProgramId, 116 | marketAuthority, 117 | }); 118 | this._middlewares.forEach((mw) => mw.initOpenOrders(ix)); 119 | return this.proxy(ix); 120 | } 121 | 122 | public cancelOrder(owner: PublicKey, order: Order): TransactionInstruction { 123 | const ix = DexInstructions.cancelOrderV2({ 124 | market: this._market.address, 125 | owner, 126 | openOrders: order.openOrdersAddress, 127 | bids: this._market.decoded.bids, 128 | asks: this._market.decoded.asks, 129 | eventQueue: this._market.decoded.eventQueue, 130 | side: order.side, 131 | orderId: order.orderId, 132 | openOrdersSlot: order.openOrdersSlot, 133 | programId: this._proxyProgramId, 134 | }); 135 | this._middlewares.forEach((mw) => mw.cancelOrderV2(ix)); 136 | return this.proxy(ix); 137 | } 138 | 139 | public cancelOrderByClientId( 140 | owner: PublicKey, 141 | openOrders: PublicKey, 142 | clientId: BN, 143 | ): TransactionInstruction { 144 | const ix = DexInstructions.cancelOrderByClientIdV2({ 145 | market: this._market.address, 146 | openOrders, 147 | owner, 148 | bids: this._market.decoded.bids, 149 | asks: this._market.decoded.asks, 150 | eventQueue: this._market.decoded.eventQueue, 151 | clientId, 152 | programId: this._proxyProgramId, 153 | }); 154 | this._middlewares.forEach((mw) => mw.cancelOrderByClientIdV2(ix)); 155 | return this.proxy(ix); 156 | } 157 | 158 | public settleFunds( 159 | openOrders: PublicKey, 160 | owner: PublicKey, 161 | baseWallet: PublicKey, 162 | quoteWallet: PublicKey, 163 | referrerQuoteWallet: PublicKey, 164 | ): TransactionInstruction { 165 | const ix = DexInstructions.settleFunds({ 166 | market: this._market.address, 167 | openOrders, 168 | owner, 169 | baseVault: this._market.decoded.baseVault, 170 | quoteVault: this._market.decoded.quoteVault, 171 | baseWallet, 172 | quoteWallet, 173 | vaultSigner: utils.publicKey.createProgramAddressSync( 174 | [ 175 | this._market.address.toBuffer(), 176 | this._market.decoded.vaultSignerNonce.toArrayLike(Buffer, 'le', 8), 177 | ], 178 | this._dexProgramId, 179 | ), 180 | programId: this._proxyProgramId, 181 | referrerQuoteWallet, 182 | }); 183 | this._middlewares.forEach((mw) => mw.settleFunds(ix)); 184 | return this.proxy(ix); 185 | } 186 | 187 | public closeOpenOrders( 188 | openOrders: PublicKey, 189 | owner: PublicKey, 190 | solWallet: PublicKey, 191 | ): TransactionInstruction { 192 | const ix = DexInstructions.closeOpenOrders({ 193 | market: this._market.address, 194 | openOrders, 195 | owner, 196 | solWallet, 197 | programId: this._proxyProgramId, 198 | }); 199 | this._middlewares.forEach((mw) => mw.closeOpenOrders(ix)); 200 | return this.proxy(ix); 201 | } 202 | 203 | public prune( 204 | openOrders: PublicKey, 205 | openOrdersOwner: PublicKey, 206 | limit?: number, 207 | ): TransactionInstruction { 208 | if (!limit) { 209 | limit = 65535; 210 | } 211 | const ix = DexInstructions.prune({ 212 | market: this._market.address, 213 | bids: this._market.decoded.bids, 214 | asks: this._market.decoded.asks, 215 | eventQueue: this._market.decoded.eventQueue, 216 | pruneAuthority: this._market.decoded.pruneAuthority, 217 | openOrders, 218 | openOrdersOwner, 219 | programId: this._proxyProgramId, 220 | limit, 221 | }); 222 | this._middlewares.forEach((mw) => mw.prune(ix)); 223 | return this.proxy(ix); 224 | } 225 | 226 | public consumeEvents( 227 | openOrdersAccounts: Array, 228 | limit: number, 229 | ): TransactionInstruction { 230 | const ix = DexInstructions.consumeEvents({ 231 | market: this._market.address, 232 | eventQueue: this._market.decoded.eventQueue, 233 | coinFee: this._market.decoded.eventQueue, 234 | pcFee: this._market.decoded.eventQueue, 235 | openOrdersAccounts, 236 | limit, 237 | programId: this._proxyProgramId, 238 | }); 239 | this._middlewares.forEach((mw) => mw.consumeEvents(ix)); 240 | return this.proxy(ix); 241 | } 242 | 243 | public consumeEventsPermissioned( 244 | openOrdersAccounts: Array, 245 | limit: number, 246 | ): TransactionInstruction { 247 | const ix = DexInstructions.consumeEventsPermissioned({ 248 | market: this._market.address, 249 | eventQueue: this._market.decoded.eventQueue, 250 | crankAuthority: this._market.decoded.consumeEventsAuthority, 251 | openOrdersAccounts, 252 | limit, 253 | programId: this._proxyProgramId, 254 | }); 255 | this._middlewares.forEach((mw) => mw.consumeEventsPermissioned(ix)); 256 | return this.proxy(ix); 257 | } 258 | 259 | // Adds the serum dex account to the instruction so that proxies can 260 | // relay (CPI requires the executable account). 261 | private proxy(ix: TransactionInstruction) { 262 | ix.keys = [ 263 | { pubkey: this._dexProgramId, isWritable: false, isSigner: false }, 264 | ...ix.keys, 265 | ]; 266 | 267 | return ix; 268 | } 269 | } 270 | 271 | export class MarketProxyBuilder { 272 | private _middlewares: Middleware[]; 273 | 274 | constructor() { 275 | this._middlewares = []; 276 | } 277 | 278 | public middleware(mw: Middleware): MarketProxyBuilder { 279 | this._middlewares.push(mw); 280 | return this; 281 | } 282 | 283 | public async load({ 284 | connection, 285 | market, 286 | options = {}, 287 | dexProgramId, 288 | proxyProgramId, 289 | }: { 290 | connection: Connection; 291 | market: PublicKey; 292 | options: MarketOptions; 293 | dexProgramId: PublicKey; 294 | proxyProgramId: PublicKey; 295 | }): Promise { 296 | const marketClient = await Market.load( 297 | connection, 298 | market, 299 | options, 300 | dexProgramId, 301 | MARKET_STATE_LAYOUT_V3, 302 | ); 303 | const instruction = new MarketProxyInstruction( 304 | proxyProgramId, 305 | dexProgramId, 306 | marketClient, 307 | this._middlewares, 308 | ); 309 | return new MarketProxy(marketClient, instruction); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /packages/serum/src/market-proxy/middleware.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@project-serum/anchor'; 2 | import { 3 | SystemProgram, 4 | PublicKey, 5 | TransactionInstruction, 6 | } from '@solana/web3.js'; 7 | 8 | export interface Middleware { 9 | initOpenOrders(ix: TransactionInstruction): void; 10 | newOrderV3(ix: TransactionInstruction): void; 11 | cancelOrderV2(ix: TransactionInstruction): void; 12 | cancelOrderByClientIdV2(ix: TransactionInstruction): void; 13 | settleFunds(ix: TransactionInstruction): void; 14 | closeOpenOrders(ix: TransactionInstruction): void; 15 | prune(ix: TransactionInstruction): void; 16 | consumeEvents(ix: TransactionInstruction): void; 17 | consumeEventsPermissioned(ix: TransactionInstruction): void; 18 | } 19 | 20 | export class OpenOrdersPda implements Middleware { 21 | private _proxyProgramId: PublicKey; 22 | private _dexProgramId: PublicKey; 23 | 24 | constructor({ 25 | proxyProgramId, 26 | dexProgramId, 27 | }: { 28 | proxyProgramId: PublicKey; 29 | dexProgramId: PublicKey; 30 | }) { 31 | this._proxyProgramId = proxyProgramId; 32 | this._dexProgramId = dexProgramId; 33 | } 34 | 35 | // PDA authorized to create open orders accounts. 36 | public static async marketAuthority( 37 | market: PublicKey, 38 | dexProgramId: PublicKey, 39 | proxyProgramId: PublicKey, 40 | ): Promise { 41 | // b"open-orders-init" 42 | const openOrdersStr = Buffer.from([ 43 | 111, 44 | 112, 45 | 101, 46 | 110, 47 | 45, 48 | 111, 49 | 114, 50 | 100, 51 | 101, 52 | 114, 53 | 115, 54 | 45, 55 | 105, 56 | 110, 57 | 105, 58 | 116, 59 | ]); 60 | const [addr] = await PublicKey.findProgramAddress( 61 | [openOrdersStr, dexProgramId.toBuffer(), market.toBuffer()], 62 | proxyProgramId, 63 | ); 64 | return addr; 65 | } 66 | 67 | public static async openOrdersAddress( 68 | market: PublicKey, 69 | owner: PublicKey, 70 | dexProgramId: PublicKey, 71 | proxyProgramId: PublicKey, 72 | ): Promise { 73 | // b"open-orders". 74 | const openOrdersStr = Buffer.from([ 75 | 111, 76 | 112, 77 | 101, 78 | 110, 79 | 45, 80 | 111, 81 | 114, 82 | 100, 83 | 101, 84 | 114, 85 | 115, 86 | ]); 87 | const [addr] = await PublicKey.findProgramAddress( 88 | [ 89 | openOrdersStr, 90 | dexProgramId.toBuffer(), 91 | market.toBuffer(), 92 | owner.toBuffer(), 93 | ], 94 | proxyProgramId, 95 | ); 96 | return addr; 97 | } 98 | 99 | initOpenOrders(ix: TransactionInstruction) { 100 | const market = ix.keys[2].pubkey; 101 | const owner = ix.keys[1].pubkey; 102 | // b"open-orders" 103 | const openOrdersSeed = Buffer.from([ 104 | 111, 105 | 112, 106 | 101, 107 | 110, 108 | 45, 109 | 111, 110 | 114, 111 | 100, 112 | 101, 113 | 114, 114 | 115, 115 | ]); 116 | 117 | // b"open-orders-init" 118 | const openOrdersInitSeed = Buffer.from([ 119 | 111, 120 | 112, 121 | 101, 122 | 110, 123 | 45, 124 | 111, 125 | 114, 126 | 100, 127 | 101, 128 | 114, 129 | 115, 130 | 45, 131 | 105, 132 | 110, 133 | 105, 134 | 116, 135 | ]); 136 | const [openOrders, bump] = utils.publicKey.findProgramAddressSync( 137 | [ 138 | openOrdersSeed, 139 | this._dexProgramId.toBuffer(), 140 | market.toBuffer(), 141 | owner.toBuffer(), 142 | ], 143 | this._proxyProgramId, 144 | ); 145 | const [marketAuthority, bumpInit] = utils.publicKey.findProgramAddressSync( 146 | [openOrdersInitSeed, this._dexProgramId.toBuffer(), market.toBuffer()], 147 | this._proxyProgramId, 148 | ); 149 | 150 | // Override the open orders account and market authority. 151 | ix.keys[0].pubkey = openOrders; 152 | ix.keys[4].pubkey = marketAuthority; 153 | 154 | // Writable because it must pay for the PDA initialization. 155 | ix.keys[1].isWritable = true; 156 | 157 | // Prepend to the account list extra accounts needed for PDA initialization. 158 | ix.keys = [ 159 | { pubkey: this._dexProgramId, isSigner: false, isWritable: false }, 160 | { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, 161 | ...ix.keys, 162 | ]; 163 | // Prepend the ix discriminator, bump, and bumpInit to the instruction data, 164 | // which saves the program compute by avoiding recalculating them in the 165 | // program. 166 | ix.data = Buffer.concat([Buffer.from([0, bump, bumpInit]), ix.data]); 167 | } 168 | newOrderV3(ix: TransactionInstruction) { 169 | ix.data = Buffer.concat([Buffer.from([1]), ix.data]); 170 | } 171 | cancelOrderV2(ix: TransactionInstruction) { 172 | ix.data = Buffer.concat([Buffer.from([2]), ix.data]); 173 | } 174 | cancelOrderByClientIdV2(ix: TransactionInstruction) { 175 | ix.data = Buffer.concat([Buffer.from([3]), ix.data]); 176 | } 177 | settleFunds(ix: TransactionInstruction) { 178 | ix.data = Buffer.concat([Buffer.from([4]), ix.data]); 179 | } 180 | closeOpenOrders(ix: TransactionInstruction) { 181 | ix.data = Buffer.concat([Buffer.from([5]), ix.data]); 182 | } 183 | prune(ix: TransactionInstruction) { 184 | ix.data = Buffer.concat([Buffer.from([6]), ix.data]); 185 | } 186 | consumeEvents(ix: TransactionInstruction) { 187 | ix.data = Buffer.concat([Buffer.from([7]), ix.data]); 188 | } 189 | consumeEventsPermissioned(ix: TransactionInstruction) { 190 | ix.data = Buffer.concat([Buffer.from([8]), ix.data]); 191 | } 192 | } 193 | 194 | export class ReferralFees implements Middleware { 195 | // eslint-disable-next-line 196 | initOpenOrders(_ix: TransactionInstruction) {} 197 | // eslint-disable-next-line 198 | newOrderV3(_ix: TransactionInstruction) {} 199 | // eslint-disable-next-line 200 | cancelOrderV2(_ix: TransactionInstruction) {} 201 | // eslint-disable-next-line 202 | cancelOrderByClientIdV2(_ix: TransactionInstruction) {} 203 | // eslint-disable-next-line 204 | settleFunds(_ix: TransactionInstruction) {} 205 | // eslint-disable-next-line 206 | closeOpenOrders(_ix: TransactionInstruction) {} 207 | // eslint-disable-next-line 208 | prune(_ix: TransactionInstruction) {} 209 | // eslint-disable-next-line 210 | consumeEvents(_ix: TransactionInstruction) {} 211 | // eslint-disable-next-line 212 | consumeEventsPermissioned(_ix: TransactionInstruction) {} 213 | } 214 | 215 | export class PermissionedCrank implements Middleware { 216 | // eslint-disable-next-line 217 | initOpenOrders(_ix: TransactionInstruction) {} 218 | // eslint-disable-next-line 219 | newOrderV3(_ix: TransactionInstruction) {} 220 | // eslint-disable-next-line 221 | cancelOrderV2(_ix: TransactionInstruction) {} 222 | // eslint-disable-next-line 223 | cancelOrderByClientIdV2(_ix: TransactionInstruction) {} 224 | // eslint-disable-next-line 225 | settleFunds(_ix: TransactionInstruction) {} 226 | // eslint-disable-next-line 227 | closeOpenOrders(_ix: TransactionInstruction) {} 228 | // eslint-disable-next-line 229 | prune(_ix: TransactionInstruction) {} 230 | // eslint-disable-next-line 231 | consumeEvents(_ix: TransactionInstruction) {} 232 | // eslint-disable-next-line 233 | consumeEventsPermissioned(ix: TransactionInstruction) { 234 | ix.keys[ix.keys.length - 1].isSigner = false; 235 | } 236 | } 237 | 238 | export class Logger implements Middleware { 239 | initOpenOrders(ix: TransactionInstruction) { 240 | console.log('Proxying initOpenOrders', this.ixToDisplay(ix)); 241 | } 242 | newOrderV3(ix: TransactionInstruction) { 243 | console.log('Proxying newOrderV3', this.ixToDisplay(ix)); 244 | } 245 | cancelOrderV2(ix: TransactionInstruction) { 246 | console.log('Proxying cancelOrderV2', this.ixToDisplay(ix)); 247 | } 248 | cancelOrderByClientIdV2(ix: TransactionInstruction) { 249 | console.log('Proxying cancelOrderByClientIdV2', this.ixToDisplay(ix)); 250 | } 251 | settleFunds(ix: TransactionInstruction) { 252 | console.log('Proxying settleFunds', this.ixToDisplay(ix)); 253 | } 254 | closeOpenOrders(ix: TransactionInstruction) { 255 | console.log('Proxying closeOpenOrders', this.ixToDisplay(ix)); 256 | } 257 | prune(ix: TransactionInstruction) { 258 | console.log('Proxying prune', this.ixToDisplay(ix)); 259 | } 260 | consumeEvents(ix: TransactionInstruction) { 261 | console.log('Proxying consumeEvents', this.ixToDisplay(ix)); 262 | } 263 | consumeEventsPermissioned(ix: TransactionInstruction) { 264 | console.log('Proxying consumeEventsPermissioned', this.ixToDisplay(ix)); 265 | } 266 | ixToDisplay(ix: TransactionInstruction): any { 267 | const keys = ix.keys.map((i) => { 268 | return { ...i, pubkey: i.pubkey.toString() }; 269 | }); 270 | const programId = ix.programId.toString(); 271 | const data = new Uint8Array(ix.data); 272 | return { keys, programId, data }; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /packages/serum/src/market.test.js: -------------------------------------------------------------------------------- 1 | import { accountFlagsLayout } from './layout'; 2 | 3 | describe('accountFlags', () => { 4 | const layout = accountFlagsLayout(); 5 | it('parses', () => { 6 | const b = Buffer.from('0000000000000000', 'hex'); 7 | expect(layout.getSpan(b)).toBe(8); 8 | expect(layout.decode(b).initialized).toBe(false); 9 | expect(layout.decode(Buffer.from('0000000000000000', 'hex'))).toMatchObject( 10 | { 11 | initialized: false, 12 | market: false, 13 | openOrders: false, 14 | requestQueue: false, 15 | eventQueue: false, 16 | bids: false, 17 | asks: false, 18 | }, 19 | ); 20 | expect(layout.decode(Buffer.from('0300000000000000', 'hex'))).toMatchObject( 21 | { 22 | initialized: true, 23 | market: true, 24 | openOrders: false, 25 | requestQueue: false, 26 | eventQueue: false, 27 | bids: false, 28 | asks: false, 29 | }, 30 | ); 31 | expect(layout.decode(Buffer.from('0500000000000000', 'hex'))).toMatchObject( 32 | { 33 | initialized: true, 34 | market: false, 35 | openOrders: true, 36 | requestQueue: false, 37 | eventQueue: false, 38 | bids: false, 39 | asks: false, 40 | }, 41 | ); 42 | }); 43 | 44 | it('serializes', () => { 45 | const b = Buffer.alloc(8); 46 | expect( 47 | layout.encode( 48 | { 49 | initialized: true, 50 | market: false, 51 | openOrders: false, 52 | requestQueue: false, 53 | eventQueue: false, 54 | bids: false, 55 | asks: true, 56 | }, 57 | b, 58 | ), 59 | ).toBe(8); 60 | expect(b.toString('hex')).toEqual('4100000000000000'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/serum/src/queue.ts: -------------------------------------------------------------------------------- 1 | import { bits, blob, struct, u32, u8 } from 'buffer-layout'; 2 | import { 3 | accountFlagsLayout, 4 | publicKeyLayout, 5 | u128, 6 | u64, 7 | zeros, 8 | } from './layout'; 9 | import BN from 'bn.js'; 10 | import { PublicKey } from '@solana/web3.js'; 11 | 12 | const REQUEST_QUEUE_HEADER = struct([ 13 | blob(5), 14 | 15 | accountFlagsLayout('accountFlags'), 16 | u32('head'), 17 | zeros(4), 18 | u32('count'), 19 | zeros(4), 20 | u32('nextSeqNum'), 21 | zeros(4), 22 | ]); 23 | 24 | const REQUEST_FLAGS = bits(u8(), false, 'requestFlags'); 25 | REQUEST_FLAGS.addBoolean('newOrder'); 26 | REQUEST_FLAGS.addBoolean('cancelOrder'); 27 | REQUEST_FLAGS.addBoolean('bid'); 28 | REQUEST_FLAGS.addBoolean('postOnly'); 29 | REQUEST_FLAGS.addBoolean('ioc'); 30 | 31 | const REQUEST = struct([ 32 | REQUEST_FLAGS, 33 | u8('openOrdersSlot'), 34 | u8('feeTier'), 35 | blob(5), 36 | u64('maxBaseSizeOrCancelId'), 37 | u64('nativeQuoteQuantityLocked'), 38 | u128('orderId'), 39 | publicKeyLayout('openOrders'), 40 | u64('clientOrderId'), 41 | ]); 42 | 43 | const EVENT_QUEUE_HEADER = struct([ 44 | blob(5), 45 | 46 | accountFlagsLayout('accountFlags'), 47 | u32('head'), 48 | zeros(4), 49 | u32('count'), 50 | zeros(4), 51 | u32('seqNum'), 52 | zeros(4), 53 | ]); 54 | 55 | const EVENT_FLAGS = bits(u8(), false, 'eventFlags'); 56 | EVENT_FLAGS.addBoolean('fill'); 57 | EVENT_FLAGS.addBoolean('out'); 58 | EVENT_FLAGS.addBoolean('bid'); 59 | EVENT_FLAGS.addBoolean('maker'); 60 | 61 | const EVENT = struct([ 62 | EVENT_FLAGS, 63 | u8('openOrdersSlot'), 64 | u8('feeTier'), 65 | blob(5), 66 | u64('nativeQuantityReleased'), // Amount the user received 67 | u64('nativeQuantityPaid'), // Amount the user paid 68 | u64('nativeFeeOrRebate'), 69 | u128('orderId'), 70 | publicKeyLayout('openOrders'), 71 | u64('clientOrderId'), 72 | ]); 73 | 74 | export interface Event { 75 | eventFlags: { fill: boolean; out: boolean; bid: boolean; maker: boolean }; 76 | 77 | seqNum?: number; 78 | orderId: BN; 79 | openOrders: PublicKey; 80 | openOrdersSlot: number; 81 | feeTier: number; 82 | 83 | nativeQuantityReleased: BN; 84 | nativeQuantityPaid: BN; 85 | nativeFeeOrRebate: BN; 86 | } 87 | 88 | function decodeQueueItem(headerLayout, nodeLayout, buffer: Buffer, nodeIndex) { 89 | return nodeLayout.decode( 90 | buffer, 91 | headerLayout.span + nodeIndex * nodeLayout.span, 92 | ); 93 | } 94 | 95 | function decodeQueue( 96 | headerLayout, 97 | nodeLayout, 98 | buffer: Buffer, 99 | history?: number, 100 | ) { 101 | const header = headerLayout.decode(buffer); 102 | const allocLen = Math.floor( 103 | (buffer.length - headerLayout.span) / nodeLayout.span, 104 | ); 105 | const nodes: any[] = []; 106 | if (history) { 107 | for (let i = 0; i < Math.min(history, allocLen); ++i) { 108 | const nodeIndex = 109 | (header.head + header.count + allocLen - 1 - i) % allocLen; 110 | nodes.push(decodeQueueItem(headerLayout, nodeLayout, buffer, nodeIndex)); 111 | } 112 | } else { 113 | for (let i = 0; i < header.count; ++i) { 114 | const nodeIndex = (header.head + i) % allocLen; 115 | nodes.push(decodeQueueItem(headerLayout, nodeLayout, buffer, nodeIndex)); 116 | } 117 | } 118 | return { header, nodes }; 119 | } 120 | 121 | export function decodeEventsSince(buffer: Buffer, lastSeqNum: number): Event[] { 122 | const header = EVENT_QUEUE_HEADER.decode(buffer); 123 | const allocLen = Math.floor( 124 | (buffer.length - EVENT_QUEUE_HEADER.span) / EVENT.span, 125 | ); 126 | 127 | // calculate number of missed events 128 | // account for u32 & ringbuffer overflows 129 | const modulo32Uint = 0x100000000; 130 | let missedEvents = (header.seqNum - lastSeqNum + modulo32Uint) % modulo32Uint; 131 | if (missedEvents > allocLen) { 132 | missedEvents = allocLen - 1; 133 | } 134 | const startSeq = (header.seqNum - missedEvents + modulo32Uint) % modulo32Uint; 135 | 136 | // define boundary indexes in ring buffer [start;end) 137 | const endIndex = (header.head + header.count) % allocLen; 138 | const startIndex = (endIndex - missedEvents + allocLen) % allocLen; 139 | 140 | const results: Event[] = []; 141 | for (let i = 0; i < missedEvents; ++i) { 142 | const nodeIndex = (startIndex + i) % allocLen; 143 | const event = decodeQueueItem(EVENT_QUEUE_HEADER, EVENT, buffer, nodeIndex); 144 | event.seqNum = (startSeq + i) % modulo32Uint; 145 | results.push(event); 146 | } 147 | return results; 148 | } 149 | 150 | export function decodeRequestQueue(buffer: Buffer, history?: number) { 151 | const { header, nodes } = decodeQueue( 152 | REQUEST_QUEUE_HEADER, 153 | REQUEST, 154 | buffer, 155 | history, 156 | ); 157 | if (!header.accountFlags.initialized || !header.accountFlags.requestQueue) { 158 | throw new Error('Invalid requests queue'); 159 | } 160 | return nodes; 161 | } 162 | 163 | export function decodeEventQueue(buffer: Buffer, history?: number): Event[] { 164 | const { header, nodes } = decodeQueue( 165 | EVENT_QUEUE_HEADER, 166 | EVENT, 167 | buffer, 168 | history, 169 | ); 170 | if (!header.accountFlags.initialized || !header.accountFlags.eventQueue) { 171 | throw new Error('Invalid events queue'); 172 | } 173 | return nodes; 174 | } 175 | 176 | export const REQUEST_QUEUE_LAYOUT = { 177 | HEADER: REQUEST_QUEUE_HEADER, 178 | NODE: REQUEST, 179 | }; 180 | 181 | export const EVENT_QUEUE_LAYOUT = { 182 | HEADER: EVENT_QUEUE_HEADER, 183 | NODE: EVENT, 184 | }; 185 | -------------------------------------------------------------------------------- /packages/serum/src/slab.test.js: -------------------------------------------------------------------------------- 1 | import { Slab } from './slab'; 2 | import BN from 'bn.js'; 3 | 4 | const SLAB_BUFFER = Buffer.from( 5 | '0900000000000000020000000000000008000000000000000400000000000000010000001e00000000000040952fe4da5c1f3c860200000004000000030000000d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d7b0000000000000000000000000000000200000002000000000000a0ca17726dae0f1e43010000001111111111111111111111111111111111111111111111111111111111111111410100000000000000000000000000000200000001000000d20a3f4eeee073c3f60fe98e010000000d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d7b000000000000000000000000000000020000000300000000000040952fe4da5c1f3c8602000000131313131313131313131313131313131313131313131313131313131313131340e20100000000000000000000000000010000001f0000000500000000000000000000000000000005000000060000000d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d7b0000000000000000000000000000000200000004000000040000000000000000000000000000001717171717171717171717171717171717171717171717171717171717171717020000000000000000000000000000000100000020000000000000a0ca17726dae0f1e430100000001000000020000000d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d7b000000000000000000000000000000040000000000000004000000000000000000000000000000171717171717171717171717171717171717171717171717171717171717171702000000000000000000000000000000030000000700000005000000000000000000000000000000171717171717171717171717171717171717171717171717171717171717171702000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', 6 | 'hex', 7 | ); 8 | 9 | describe('slab', () => { 10 | let slab; 11 | 12 | it('parses', () => { 13 | slab = Slab.decode(SLAB_BUFFER); 14 | expect(slab).toBeTruthy(); 15 | expect(slab.header.bumpIndex).toBe(9); 16 | expect(slab.nodes).toHaveLength(9); 17 | }); 18 | 19 | it('finds nodes', () => { 20 | expect(slab.get(new BN('123456789012345678901234567890')).ownerSlot).toBe( 21 | 1, 22 | ); 23 | expect(slab.get(new BN('100000000000000000000000000000')).ownerSlot).toBe( 24 | 2, 25 | ); 26 | expect(slab.get(new BN('200000000000000000000000000000')).ownerSlot).toBe( 27 | 3, 28 | ); 29 | expect(slab.get(4).ownerSlot).toBe(4); 30 | }); 31 | 32 | it('does not find nonexistant nodes', () => { 33 | expect(slab.get(0)).toBeNull(); 34 | expect(slab.get(3)).toBeNull(); 35 | expect(slab.get(5)).toBeNull(); 36 | expect(slab.get(6)).toBeNull(); 37 | expect(slab.get(new BN('200000000000000000000000000001'))).toBeNull(); 38 | expect(slab.get(new BN('100000000000000000000000000001'))).toBeNull(); 39 | expect(slab.get(new BN('123456789012345678901234567889'))).toBeNull(); 40 | expect(slab.get(new BN('123456789012345678901234567891'))).toBeNull(); 41 | expect(slab.get(new BN('99999999999999999999999999999'))).toBeNull(); 42 | }); 43 | 44 | it('iterates', () => { 45 | expect(Array.from(slab)).toHaveLength(4); 46 | }); 47 | 48 | it('iterates in order', () => { 49 | let previous = null; 50 | for (const item of slab) { 51 | if (previous) { 52 | expect(item.key.gt(previous.key)).toBeTruthy(); 53 | } 54 | previous = item; 55 | } 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/serum/src/slab.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { blob, offset, seq, struct, u32, u8, union } from 'buffer-layout'; 3 | import { publicKeyLayout, setLayoutDecoder, u128, u64, zeros } from './layout'; 4 | import { PublicKey } from '@solana/web3.js'; 5 | 6 | const SLAB_HEADER_LAYOUT = struct( 7 | [ 8 | // Number of modified slab nodes 9 | u32('bumpIndex'), 10 | zeros(4), // Consider slabs with more than 2^32 nodes to be invalid 11 | 12 | // Linked list of unused nodes 13 | u32('freeListLen'), 14 | zeros(4), 15 | u32('freeListHead'), 16 | 17 | u32('root'), 18 | 19 | u32('leafCount'), 20 | zeros(4), 21 | ], 22 | 'header', 23 | ); 24 | 25 | const SLAB_NODE_LAYOUT = union(u32('tag'), blob(68), 'node'); 26 | SLAB_NODE_LAYOUT.addVariant(0, struct([]), 'uninitialized'); 27 | SLAB_NODE_LAYOUT.addVariant( 28 | 1, 29 | struct([ 30 | // Only the first prefixLen high-order bits of key are meaningful 31 | u32('prefixLen'), 32 | u128('key'), 33 | seq(u32(), 2, 'children'), 34 | ]), 35 | 'innerNode', 36 | ); 37 | SLAB_NODE_LAYOUT.addVariant( 38 | 2, 39 | struct([ 40 | u8('ownerSlot'), // Index into OPEN_ORDERS_LAYOUT.orders 41 | u8('feeTier'), 42 | blob(2), 43 | u128('key'), // (price, seqNum) 44 | publicKeyLayout('owner'), // Open orders account 45 | u64('quantity'), // In units of lot size 46 | u64('clientOrderId'), 47 | ]), 48 | 'leafNode', 49 | ); 50 | SLAB_NODE_LAYOUT.addVariant(3, struct([u32('next')]), 'freeNode'); 51 | SLAB_NODE_LAYOUT.addVariant(4, struct([]), 'lastFreeNode'); 52 | 53 | export const SLAB_LAYOUT = struct([ 54 | SLAB_HEADER_LAYOUT, 55 | seq( 56 | SLAB_NODE_LAYOUT, 57 | offset( 58 | SLAB_HEADER_LAYOUT.layoutFor('bumpIndex'), 59 | SLAB_HEADER_LAYOUT.offsetOf('bumpIndex') - SLAB_HEADER_LAYOUT.span, 60 | ), 61 | 'nodes', 62 | ), 63 | ]); 64 | 65 | export class Slab { 66 | private header: any; 67 | private nodes: any; 68 | 69 | constructor(header, nodes) { 70 | this.header = header; 71 | this.nodes = nodes; 72 | } 73 | 74 | static decode(buffer: Buffer) { 75 | return SLAB_LAYOUT.decode(buffer); 76 | } 77 | 78 | get(searchKey: BN | number) { 79 | if (this.header.leafCount === 0) { 80 | return null; 81 | } 82 | if (!(searchKey instanceof BN)) { 83 | searchKey = new BN(searchKey); 84 | } 85 | let index = this.header.root; 86 | while (true) { 87 | const { leafNode, innerNode } = this.nodes[index]; 88 | if (leafNode) { 89 | if (leafNode.key.eq(searchKey)) { 90 | return leafNode; 91 | } 92 | return null; 93 | } else if (innerNode) { 94 | if ( 95 | !innerNode.key 96 | .xor(searchKey) 97 | .iushrn(128 - innerNode.prefixLen) 98 | .isZero() 99 | ) { 100 | return null; 101 | } 102 | index = 103 | innerNode.children[ 104 | searchKey.testn(128 - innerNode.prefixLen - 1) ? 1 : 0 105 | ]; 106 | } else { 107 | throw new Error('Invalid slab'); 108 | } 109 | } 110 | } 111 | 112 | [Symbol.iterator]() { 113 | return this.items(false); 114 | } 115 | 116 | *items( 117 | descending = false, 118 | ): Generator<{ 119 | ownerSlot: number; 120 | key: BN; 121 | owner: PublicKey; 122 | quantity: BN; 123 | feeTier: number; 124 | clientOrderId: BN; 125 | }> { 126 | if (this.header.leafCount === 0) { 127 | return; 128 | } 129 | const stack = [this.header.root]; 130 | while (stack.length > 0) { 131 | const index = stack.pop(); 132 | const { leafNode, innerNode } = this.nodes[index]; 133 | if (leafNode) { 134 | yield leafNode; 135 | } else if (innerNode) { 136 | if (descending) { 137 | stack.push(innerNode.children[0], innerNode.children[1]); 138 | } else { 139 | stack.push(innerNode.children[1], innerNode.children[0]); 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | setLayoutDecoder(SLAB_LAYOUT, ({ header, nodes }) => new Slab(header, nodes)); 147 | -------------------------------------------------------------------------------- /packages/serum/src/token-instructions.js: -------------------------------------------------------------------------------- 1 | import * as BufferLayout from 'buffer-layout'; 2 | import { 3 | PublicKey, 4 | SYSVAR_RENT_PUBKEY, 5 | TransactionInstruction, 6 | } from '@solana/web3.js'; 7 | import { publicKeyLayout } from './layout'; 8 | 9 | // NOTE: Update these if the position of arguments for the initializeAccount instruction changes 10 | export const INITIALIZE_ACCOUNT_ACCOUNT_INDEX = 0; 11 | export const INITIALIZE_ACCOUNT_MINT_INDEX = 1; 12 | export const INITIALIZE_ACCOUNT_OWNER_INDEX = 2; 13 | 14 | // NOTE: Update these if the position of arguments for the transfer instruction changes 15 | export const TRANSFER_SOURCE_INDEX = 0; 16 | export const TRANSFER_DESTINATION_INDEX = 1; 17 | export const TRANSFER_OWNER_INDEX = 2; 18 | 19 | // NOTE: Update these if the position of arguments for the closeAccount instruction changes 20 | export const CLOSE_ACCOUNT_SOURCE_INDEX = 0; 21 | export const CLOSE_ACCOUNT_DESTINATION_INDEX = 1; 22 | export const CLOSE_ACCOUNT_OWNER_INDEX = 2; 23 | 24 | export const TOKEN_PROGRAM_ID = new PublicKey( 25 | 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 26 | ); 27 | 28 | export const WRAPPED_SOL_MINT = new PublicKey( 29 | 'So11111111111111111111111111111111111111112', 30 | ); 31 | 32 | export const MSRM_MINT = new PublicKey( 33 | 'MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L', 34 | ); 35 | export const MSRM_DECIMALS = 0; 36 | 37 | export const SRM_MINT = new PublicKey( 38 | 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt', 39 | ); 40 | export const SRM_DECIMALS = 6; 41 | 42 | const LAYOUT = BufferLayout.union(BufferLayout.u8('instruction')); 43 | LAYOUT.addVariant( 44 | 0, 45 | BufferLayout.struct([ 46 | BufferLayout.u8('decimals'), 47 | publicKeyLayout('mintAuthority'), 48 | BufferLayout.u8('freezeAuthorityOption'), 49 | publicKeyLayout('freezeAuthority'), 50 | ]), 51 | 'initializeMint', 52 | ); 53 | LAYOUT.addVariant(1, BufferLayout.struct([]), 'initializeAccount'); 54 | LAYOUT.addVariant( 55 | 3, 56 | BufferLayout.struct([BufferLayout.nu64('amount')]), 57 | 'transfer', 58 | ); 59 | LAYOUT.addVariant( 60 | 4, 61 | BufferLayout.struct([BufferLayout.nu64('amount')]), 62 | 'approve', 63 | ); 64 | LAYOUT.addVariant(5, BufferLayout.struct([]), 'revoke'); 65 | LAYOUT.addVariant( 66 | 6, 67 | BufferLayout.struct([ 68 | BufferLayout.u8('authorityType'), 69 | BufferLayout.u8('newAuthorityOption'), 70 | publicKeyLayout('newAuthority'), 71 | ]), 72 | 'setAuthority', 73 | ); 74 | LAYOUT.addVariant( 75 | 7, 76 | BufferLayout.struct([BufferLayout.nu64('amount')]), 77 | 'mintTo', 78 | ); 79 | LAYOUT.addVariant( 80 | 8, 81 | BufferLayout.struct([BufferLayout.nu64('amount')]), 82 | 'burn', 83 | ); 84 | LAYOUT.addVariant(9, BufferLayout.struct([]), 'closeAccount'); 85 | 86 | const instructionMaxSpan = Math.max( 87 | ...Object.values(LAYOUT.registry).map((r) => r.span), 88 | ); 89 | 90 | function encodeTokenInstructionData(instruction) { 91 | const b = Buffer.alloc(instructionMaxSpan); 92 | const span = LAYOUT.encode(instruction, b); 93 | return b.slice(0, span); 94 | } 95 | 96 | export function decodeTokenInstructionData(instruction) { 97 | return LAYOUT.decode(instruction); 98 | } 99 | 100 | export function initializeMint({ 101 | mint, 102 | decimals, 103 | mintAuthority, 104 | freezeAuthority = null, 105 | }) { 106 | const keys = [ 107 | { pubkey: mint, isSigner: false, isWritable: true }, 108 | { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, 109 | ]; 110 | return new TransactionInstruction({ 111 | keys, 112 | data: encodeTokenInstructionData({ 113 | initializeMint: { 114 | decimals, 115 | mintAuthority, 116 | freezeAuthorityOption: !!freezeAuthority, 117 | freezeAuthority: freezeAuthority || new PublicKey(0), 118 | }, 119 | }), 120 | programId: TOKEN_PROGRAM_ID, 121 | }); 122 | } 123 | 124 | export function initializeAccount({ account, mint, owner }) { 125 | const keys = [ 126 | { pubkey: account, isSigner: false, isWritable: true }, 127 | { pubkey: mint, isSigner: false, isWritable: false }, 128 | { pubkey: owner, isSigner: false, isWritable: false }, 129 | { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, 130 | ]; 131 | return new TransactionInstruction({ 132 | keys, 133 | data: encodeTokenInstructionData({ 134 | initializeAccount: {}, 135 | }), 136 | programId: TOKEN_PROGRAM_ID, 137 | }); 138 | } 139 | 140 | export function transfer({ source, destination, amount, owner }) { 141 | const keys = [ 142 | { pubkey: source, isSigner: false, isWritable: true }, 143 | { pubkey: destination, isSigner: false, isWritable: true }, 144 | { pubkey: owner, isSigner: true, isWritable: false }, 145 | ]; 146 | return new TransactionInstruction({ 147 | keys, 148 | data: encodeTokenInstructionData({ 149 | transfer: { amount }, 150 | }), 151 | programId: TOKEN_PROGRAM_ID, 152 | }); 153 | } 154 | 155 | export function approve({ source, delegate, amount, owner }) { 156 | const keys = [ 157 | { pubkey: source, isSigner: false, isWritable: true }, 158 | { pubkey: delegate, isSigner: false, isWritable: false }, 159 | { pubkey: owner, isSigner: true, isWritable: false }, 160 | ]; 161 | return new TransactionInstruction({ 162 | keys, 163 | data: encodeTokenInstructionData({ 164 | approve: { amount }, 165 | }), 166 | programId: TOKEN_PROGRAM_ID, 167 | }); 168 | } 169 | 170 | export function revoke({ source, owner }) { 171 | const keys = [ 172 | { pubkey: source, isSigner: false, isWritable: true }, 173 | { pubkey: owner, isSigner: true, isWritable: false }, 174 | ]; 175 | return new TransactionInstruction({ 176 | keys, 177 | data: encodeTokenInstructionData({ 178 | revoke: {}, 179 | }), 180 | programId: TOKEN_PROGRAM_ID, 181 | }); 182 | } 183 | 184 | export function setAuthority({ 185 | target, 186 | currentAuthority, 187 | newAuthority, 188 | authorityType, 189 | }) { 190 | const keys = [ 191 | { pubkey: target, isSigner: false, isWritable: true }, 192 | { pubkey: currentAuthority, isSigner: true, isWritable: false }, 193 | ]; 194 | return new TransactionInstruction({ 195 | keys, 196 | data: encodeTokenInstructionData({ 197 | setAuthority: { 198 | authorityType, 199 | newAuthorityOption: !!newAuthority, 200 | newAuthority, 201 | }, 202 | }), 203 | programId: TOKEN_PROGRAM_ID, 204 | }); 205 | } 206 | 207 | export function mintTo({ mint, destination, amount, mintAuthority }) { 208 | const keys = [ 209 | { pubkey: mint, isSigner: false, isWritable: true }, 210 | { pubkey: destination, isSigner: false, isWritable: true }, 211 | { pubkey: mintAuthority, isSigner: true, isWritable: false }, 212 | ]; 213 | return new TransactionInstruction({ 214 | keys, 215 | data: encodeTokenInstructionData({ 216 | mintTo: { amount }, 217 | }), 218 | programId: TOKEN_PROGRAM_ID, 219 | }); 220 | } 221 | 222 | export function closeAccount({ source, destination, owner }) { 223 | const keys = [ 224 | { pubkey: source, isSigner: false, isWritable: true }, 225 | { pubkey: destination, isSigner: false, isWritable: true }, 226 | { pubkey: owner, isSigner: true, isWritable: false }, 227 | ]; 228 | return new TransactionInstruction({ 229 | keys, 230 | data: encodeTokenInstructionData({ 231 | closeAccount: {}, 232 | }), 233 | programId: TOKEN_PROGRAM_ID, 234 | }); 235 | } 236 | -------------------------------------------------------------------------------- /packages/serum/src/token-mints.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "address": "9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E", 4 | "name": "BTC" 5 | }, 6 | { 7 | "address": "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk", 8 | "name": "ETH" 9 | }, 10 | { 11 | "address": "AGFEad2et2ZJif9jaGpdMixQqvW5i81aBdvKe7PHNfz3", 12 | "name": "FTT" 13 | }, 14 | { 15 | "address": "3JSf5tPeuscJGtaCp5giEiDhv51gQ4v3zWg8DGgyLfAB", 16 | "name": "YFI" 17 | }, 18 | { 19 | "address": "CWE8jPTUYhdCTZYWPTe1o5DFqfdjzWKc9WKz6rSjQUdG", 20 | "name": "LINK" 21 | }, 22 | { 23 | "address": "Ga2AXHpfAF6mv2ekZwcsJFqu7wB4NV331qNH7fW9Nst8", 24 | "name": "XRP" 25 | }, 26 | { 27 | "address": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", 28 | "name": "USDT" 29 | }, 30 | { 31 | "address": "BQcdHdAQW1hczDbBi9hiegXAR7A98Q9jx3X3iBBBDiq4", 32 | "name": "WUSDT" 33 | }, 34 | { 35 | "address": "BXXkv6z8ykpG1yuvUDPgh732wzVHB69RnB9YgSYh3itW", 36 | "name": "WUSDC" 37 | }, 38 | { 39 | "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 40 | "name": "USDC" 41 | }, 42 | { 43 | "address": "MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L", 44 | "name": "MSRM" 45 | }, 46 | { 47 | "address": "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", 48 | "name": "SRM" 49 | }, 50 | { 51 | "address": "AR1Mtgh7zAtxuxGd2XPovXPVjcSdY3i4rQYisNadjfKy", 52 | "name": "SUSHI" 53 | }, 54 | { 55 | "address": "SF3oTvfWzEP3DTwGSvUXRrGTvr75pdZNnBLAH9bzMuX", 56 | "name": "SXP" 57 | }, 58 | { 59 | "address": "CsZ5LZkDS7h9TDKjrbL7VAwQZ9nsRu8vJLhRYfmGaN8K", 60 | "name": "ALEPH" 61 | }, 62 | { 63 | "address": "BtZQfWqDGbk9Wf2rXEiWyQBdBY1etnUUn6zEphvVS7yN", 64 | "name": "HGET" 65 | }, 66 | { 67 | "address": "5Fu5UUgbjpUvdBveb3a1JTNirL8rXtiYeSMWvKjtUNQv", 68 | "name": "CREAM" 69 | }, 70 | { 71 | "address": "873KLxCbz7s9Kc4ZzgYRtNmhfkQrhfyWGZJBmyCbC3ei", 72 | "name": "UBXT" 73 | }, 74 | { 75 | "address": "HqB7uswoVg4suaQiDP3wjxob1G5WdZ144zhdStwMCq7e", 76 | "name": "HNT" 77 | }, 78 | { 79 | "address": "9S4t2NEAiJVMvPdRYKVrfJpBafPBLtvbvyS3DecojQHw", 80 | "name": "FRONT" 81 | }, 82 | { 83 | "address": "6WNVCuxCGJzNjmMZoKyhZJwvJ5tYpsLyAtagzYASqBoF", 84 | "name": "AKRO" 85 | }, 86 | { 87 | "address": "DJafV9qemGp7mLMEn5wrfqaFwxsbLgUsGVS16zKRk9kc", 88 | "name": "HXRO" 89 | }, 90 | { 91 | "address": "DEhAasscXF4kEGxFgJ3bq4PpVGp5wyUxMRvn6TzGVHaw", 92 | "name": "UNI" 93 | }, 94 | { 95 | "address": "GUohe4DJUA5FKPWo3joiPgsB7yzer7LpDmt1Vhzy3Zht", 96 | "name": "KEEP" 97 | }, 98 | { 99 | "address": "GeDS162t9yGJuLEHPWXXGrb1zwkzinCgRwnT8vHYjKza", 100 | "name": "MATH" 101 | }, 102 | { 103 | "address": "So11111111111111111111111111111111111111112", 104 | "name": "SOL" 105 | }, 106 | { 107 | "address": "GXMvfY2jpQctDqZ9RoU3oWPhufKiCcFEfchvYumtX7jd", 108 | "name": "TOMO" 109 | }, 110 | { 111 | "address": "EqWCKXfs3x47uVosDpTRgFniThL9Y8iCztJaapxbEaVX", 112 | "name": "LUA" 113 | }, 114 | { 115 | "address": "9F9fNTT6qwjsu4X4yWYKZpsbw5qT7o6yR2i57JF2jagy", 116 | "name": "SWAG" 117 | }, 118 | { 119 | "address": "EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp", 120 | "name": "FIDA" 121 | }, 122 | { 123 | "address": "kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6", 124 | "name": "KIN" 125 | }, 126 | { 127 | "address": "MAPS41MDahZ9QdKXhVa4dWB9RuyfV4XqhyAZ8XcYepb", 128 | "name": "MAPS" 129 | }, 130 | { 131 | "address": "z3dn17yLaGMKffVogeFHQ9zWVcXgqgf3PQnDsNs2g6M", 132 | "name": "OXY" 133 | }, 134 | { 135 | "address": "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R", 136 | "name": "RAY" 137 | }, 138 | { 139 | "address": "3K6rftdAaQYMPunrtNRHgnK2UAtjm2JwyT2oCiTDouYE", 140 | "name": "xCOPE" 141 | }, 142 | { 143 | "address": "dK83wTVypEpa1pqiBbHY3MNuUnT3ADUZM4wk9VZXZEc", 144 | "name": "AAVE" 145 | }, 146 | { 147 | "address": "DgHK9mfhMtUwwv54GChRrU54T2Em5cuszq2uMuen1ZVE", 148 | "name": "CEL" 149 | }, 150 | { 151 | "address": "7ncCLJpP3MNww17LW8bRvx8odQQnubNtfNZBL5BgAEHW", 152 | "name": "RSR" 153 | }, 154 | { 155 | "address": "6ry4WBDvAwAnrYJVv6MCog4J8zx6S3cPgSqnTsDZ73AR", 156 | "name": "TRYB" 157 | }, 158 | { 159 | "address": "8HGyAAB1yoM1ttS7pXjHMa3dukTFGQggnFFH3hJZgzQh", 160 | "name": "COPE" 161 | }, 162 | { 163 | "address": "MERt85fc5boKw3BW1eYdxonEuJNvXbiMbs6hvheau5K", 164 | "name": "MER" 165 | }, 166 | { 167 | "address": "4dmKkXNHdgYsXqBHCuMikNQWwVomZURhYvkkX5c4pQ7y", 168 | "name": "SNY" 169 | }, 170 | { 171 | "address": "SLRSSpSLUTP7okbCUBYStWCo1vUgyt775faPqz8HUMr", 172 | "name": "SLRS" 173 | }, 174 | { 175 | "address": "CDJWUqTcYTVAKXAVXoQZFes5JUFc7owSeq7eMQcDSbo5", 176 | "name": "renBTC" 177 | }, 178 | { 179 | "address": "ArUkYE2XDKzqy77PRRGjo4wREWwqk6RXTfM9NeqzPvjU", 180 | "name": "renDOGE" 181 | }, 182 | { 183 | "address": "GsNzxJfFn6zQdJGeYsupJWzUAm57Ba7335mfhWvFiE9Z", 184 | "name": "DXL" 185 | } 186 | ] 187 | -------------------------------------------------------------------------------- /packages/serum/src/tokens_and_markets.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import Markets from './markets.json'; 3 | import TokenMints from './token-mints.json'; 4 | 5 | export const PROGRAM_LAYOUT_VERSIONS = { 6 | '4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn': 1, 7 | BJ3jrUzddfuSrZHXSCxMUUQsjKEyLmuuyZebkcaFp2fg: 1, 8 | EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o: 2, 9 | '9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin': 3, 10 | }; 11 | 12 | export function getLayoutVersion(programId: PublicKey) { 13 | return PROGRAM_LAYOUT_VERSIONS[programId.toString()] || 3; 14 | } 15 | 16 | export const TOKEN_MINTS: Array<{ 17 | address: PublicKey; 18 | name: string; 19 | }> = TokenMints.map((mint) => { 20 | return { 21 | address: new PublicKey(mint.address), 22 | name: mint.name, 23 | }; 24 | }); 25 | 26 | export const MARKETS: Array<{ 27 | address: PublicKey; 28 | name: string; 29 | programId: PublicKey; 30 | deprecated: boolean; 31 | }> = Markets.map((market) => { 32 | return { 33 | address: new PublicKey(market.address), 34 | name: market.name, 35 | programId: new PublicKey(market.programId), 36 | deprecated: market.deprecated, 37 | }; 38 | }); 39 | -------------------------------------------------------------------------------- /packages/serum/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node12/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "noImplicitAny": false, 10 | "resolveJsonModule": true 11 | }, 12 | "include": ["./src/**/*"], 13 | "exclude": ["./src/**/*.test.js", "node_modules", "**/node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/spl-token-swap/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.1.0] 9 | 10 | ### Breaking 11 | 12 | - Renamed package from `@project-serum/swap` to `@project-serum/spl-token-swap`. 13 | 14 | ## [0.0.5] 15 | 16 | ### Added 17 | 18 | - `CurveType` enum - enables support for custom curves. Currently supported: 19 | - ConstantProduct = 0 20 | - ConstantPrice = 1 21 | - Stable = 2 22 | - ConstantProductWithOffset = 3 23 | - `token_b_offset` and `token_b_price` in `PoolConfig` 24 | - `withdrawExactOneInstruction` - support for single-side withdrawals 25 | - `depositExactOneInstruction` - support for single-side deposits 26 | - Used ephemeral authority for all transfers 27 | 28 | ### Changed 29 | 30 | - Moved fees fields into a nested property `fees` inside `PoolConfig` 31 | - `initializePool` requires now programId to support multiple contracts 32 | - `makeInitializePoolTransaction` requires now programId to support multiple contracts 33 | - `swapInstruction` added transferAuthority 34 | - `depositInstruction` added transferAuthority 35 | - `withdrawInstruction` added transferAuthority 36 | - `createInitSwapInstruction` uses PoolConfig object instead of individual fields 37 | 38 | ### Removed 39 | -------------------------------------------------------------------------------- /packages/spl-token-swap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-serum/spl-token-swap", 3 | "version": "0.1.0-alpha.1", 4 | "description": "Serum Swap", 5 | "main": "lib/index.js", 6 | "types": "lib/src/index.d.ts", 7 | "license": "Apache-2.0", 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "scripts": { 12 | "build": "tsc", 13 | "test": "jest test", 14 | "coverage": "jest --coverage test", 15 | "prepublishOnly": "yarn build", 16 | "shell": "tsc && node -e \"$(< shell)\" -i --experimental-repl-await" 17 | }, 18 | "jest": { 19 | "transform": { 20 | ".(ts)": "ts-jest" 21 | }, 22 | "testEnvironment": "node", 23 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts)$", 24 | "moduleFileExtensions": [ 25 | "ts", 26 | "tsx", 27 | "js" 28 | ] 29 | }, 30 | "dependencies": { 31 | "@project-serum/serum": "^0.13.21", 32 | "@solana/spl-token": "^0.0.13", 33 | "@solana/spl-token-swap": "0.1.0", 34 | "bn.js": "^5.1.3", 35 | "bs58": "^4.0.1", 36 | "buffer-layout": "^1.2.0", 37 | "dotenv": "^8.2.0" 38 | }, 39 | "peerDependencies": { 40 | "@solana/web3.js": "^0.90.0" 41 | }, 42 | "devDependencies": { 43 | "@tsconfig/node12": "^1.0.7", 44 | "@types/bn.js": "^4.11.6", 45 | "@types/bs58": "^4.0.1", 46 | "@types/jest": "^24.9.1", 47 | "@types/node": "^12.12.62" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/spl-token-swap/shell: -------------------------------------------------------------------------------- 1 | const lib = require('./lib/index.js'); 2 | const solana = require('@solana/web3.js'); 3 | const serum = require('@project-serum/serum'); 4 | 5 | const connection = new solana.Connection('https://solana-api.projectserum.com'); 6 | -------------------------------------------------------------------------------- /packages/spl-token-swap/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pools'; 2 | export * from './instructions'; 3 | export * from './utils'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /packages/spl-token-swap/src/types.ts: -------------------------------------------------------------------------------- 1 | import { AccountInfo, Commitment, PublicKey } from '@solana/web3.js'; 2 | 3 | import { AccountInfo as TokenAccountInfo } from '@solana/spl-token'; 4 | 5 | export interface TokenAccount { 6 | pubkey: PublicKey; 7 | account: AccountInfo; 8 | info: TokenAccountInfo; 9 | } 10 | 11 | export enum CurveType { 12 | ConstantProduct = 0, 13 | ConstantPrice = 1, 14 | Stable = 2, 15 | ConstantProductWithOffset = 3, 16 | } 17 | 18 | export interface PoolConfig { 19 | curveType: CurveType; 20 | fees: { 21 | tradeFeeNumerator: number; 22 | tradeFeeDenominator: number; 23 | ownerTradeFeeNumerator: number; 24 | ownerTradeFeeDenominator: number; 25 | ownerWithdrawFeeNumerator: number; 26 | ownerWithdrawFeeDenominator: number; 27 | hostFeeNumerator: number; 28 | hostFeeDenominator: number; 29 | }; 30 | 31 | token_b_offset?: number; 32 | token_b_price?: number; 33 | } 34 | 35 | export interface PoolOptions { 36 | skipPreflight?: boolean; 37 | commitment?: Commitment; 38 | } 39 | -------------------------------------------------------------------------------- /packages/spl-token-swap/src/utils.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | 3 | export const timeMs = (): number => { 4 | return new Date().getTime(); 5 | }; 6 | 7 | export function divideBnToNumber(numerator: BN, denominator: BN): number { 8 | const quotient = numerator.div(denominator).toNumber(); 9 | const rem = numerator.umod(denominator); 10 | const gcd = rem.gcd(denominator); 11 | return quotient + rem.div(gcd).toNumber() / denominator.div(gcd).toNumber(); 12 | } 13 | 14 | export function getTokenMultiplierFromDecimals(decimals: number): BN { 15 | return new BN(10).pow(new BN(decimals)); 16 | } 17 | -------------------------------------------------------------------------------- /packages/spl-token-swap/test/example.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Example test suite', () => { 2 | it('Works', async () => { 3 | // no-op 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/spl-token-swap/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node12/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "noImplicitAny": false, 10 | "resolveJsonModule": true, 11 | "sourceMap": true 12 | }, 13 | "include": ["./src/**/*"], 14 | "exclude": ["./src/**/*.test.js", "node_modules", "**/node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/swap/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /packages/swap/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier", 8 | "prettier/@typescript-eslint" 9 | ], 10 | "env": { 11 | "node": true, 12 | "jest": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "no-constant-condition": ["error", { "checkLoops": false }], 20 | "@typescript-eslint/explicit-module-boundary-types": "off", 21 | "@typescript-eslint/ban-ts-comment": "off", 22 | "@typescript-eslint/no-explicit-any": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/swap/README.md: -------------------------------------------------------------------------------- 1 | # Serum Swap 2 | 3 | [![Build Status](https://travis-ci.com/project-serum/serum-ts.svg?branch=master)](https://travis-ci.com/project-serum/serum-ts) 4 | [![npm (scoped)](https://img.shields.io/npm/v/@project-serum/swap)](https://www.npmjs.com/package/@project-serum/swap) 5 | [![Discord Chat](https://img.shields.io/discord/739225212658122886?color=blueviolet)](https://discord.com/channels/739225212658122886) 6 | [![Documentation](https://img.shields.io/badge/typedoc-documentation-blue)](https://project-serum.github.io/serum-ts/swap/classes/swap.html) 7 | [![License](https://img.shields.io/github/license/project-serum/serum-dex?color=blue)](https://opensource.org/licenses/Apache-2.0) 8 | 9 | Client library for swapping directly on the serum orderbook. 10 | The Solana program can be found [here](https://github.com/project-serum/swap). 11 | 12 | ## Installation 13 | 14 | Using npm: 15 | 16 | ``` 17 | npm install @solana/web3.js @project-serum/swap 18 | ``` 19 | 20 | Using yarn: 21 | 22 | ``` 23 | yarn add @solana/web3.js @project-serum/swap 24 | ``` 25 | 26 | ## API Reference 27 | 28 | [API Reference](https://project-serum.github.io/serum-ts/swap/classes/swap.html). 29 | -------------------------------------------------------------------------------- /packages/swap/examples/swap.js: -------------------------------------------------------------------------------- 1 | const anchor = require('@project-serum/anchor'); 2 | const Provider = anchor.Provider; 3 | const Wallet = anchor.Wallet; 4 | const BN = anchor.BN; 5 | const Connection = require('@solana/web3.js').Connection; 6 | const PublicKey = require('@solana/web3.js').PublicKey; 7 | const TokenListProvider = require('@solana/spl-token-registry') 8 | .TokenListProvider; 9 | const Swap = require('..').Swap; 10 | 11 | // Mainnet beta addresses. 12 | const SRM = new PublicKey('SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt'); 13 | const USDC = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'); 14 | const USDT = new PublicKey('Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'); 15 | const WBTC = new PublicKey('9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'); 16 | const DECIMALS = 6; 17 | 18 | async function main() { 19 | // Client for sending transactions to the swap program on mainnet. 20 | const client = await swapClient(); 21 | 22 | // All tokens available for swapping. 23 | const _tokens = client.tokens(); 24 | 25 | // All tokens available for swapping with SRM. 26 | const _srmSwapPairs = client.pairs(SRM); 27 | 28 | // Estimate the amount received by swapping from SRM -> USDC. 29 | const estimatedUsdc = await client.estimate({ 30 | fromMint: SRM, 31 | toMint: USDC, 32 | amount: toNative(1), 33 | }); 34 | 35 | const estimatedBtc = await client.estimate({ 36 | fromMint: SRM, 37 | toMint: WBTC, 38 | amount: toNative(1), 39 | }); 40 | console.log('estimate', estimatedBtc.toNumber()); 41 | /* 42 | // Swaps SRM -> USDC on the Serum orderbook. If the resulting USDC is 43 | // has greater than a 1% error from the estimate, then fails. 44 | const usdcSwapTx = await client.swap({ 45 | fromMint: SRM, 46 | toMint: USDC, 47 | amount: toNative(1), 48 | minExpectedSwapAmount: estimatedUsdc.mul(new BN(99)).div(new BN(100)), 49 | }); 50 | 51 | // Uses the default minExpectedSwapAmount calculation. 52 | const usdcSwapTxDefault = await client.swap({ 53 | fromMint: SRM, 54 | toMint: USDC, 55 | amount: toNative(1), 56 | }); 57 | 58 | // Transitive swap from SRM -> USDC -> BTC. 59 | const btcSwapTx = await client.swap({ 60 | fromMint: SRM, 61 | toMint: WBTC, 62 | amount: toNative(1), 63 | }); 64 | 65 | console.log('resp', fromNative(estimatedUsdc)); 66 | console.log('resp', fromNative(estimatedBtc)); 67 | console.log('resp', usdcSwapTx); 68 | console.log('resp', usdcSwapTxDefault); 69 | console.log('resp', btcSwapTx); 70 | */ 71 | } 72 | 73 | async function swapClient() { 74 | const provider = new Provider( 75 | new Connection('https://api.mainnet-beta.solana.com', 'recent'), 76 | Wallet.local(), 77 | Provider.defaultOptions(), 78 | ); 79 | const tokenList = await new TokenListProvider().resolve(); 80 | return new Swap(provider, tokenList); 81 | } 82 | 83 | // Converts the given number to native units (i.e. with decimals). 84 | // The mints used in this example all have 6 decimals. One should dynamically 85 | // fetch decimals for the tokens they are swapping in production. 86 | function toNative(amount) { 87 | return new BN(amount * 10 ** DECIMALS); 88 | } 89 | 90 | function fromNative(amount) { 91 | return amount.toNumber() / 10 ** DECIMALS; 92 | } 93 | 94 | main().catch(console.error); 95 | -------------------------------------------------------------------------------- /packages/swap/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/js-with-ts', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/swap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-serum/swap", 3 | "version": "0.1.0-alpha.1", 4 | "description": "Client for swapping on the Serum DEX", 5 | "license": "MIT", 6 | "repository": "project-serum/serum-ts", 7 | "main": "lib/index.js", 8 | "source": "src/index.js", 9 | "types": "lib/index.d.ts", 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "scripts": { 14 | "build": "tsc", 15 | "start": "tsc --watch", 16 | "clean": "rm -rf lib", 17 | "docs": "typedoc --excludePrivate --out ../../docs/swap src/index.ts --includeVersion --readme none", 18 | "prepare": "run-s clean build", 19 | "shell": "node -e \"$(< shell)\" -i --experimental-repl-await" 20 | }, 21 | "devDependencies": { 22 | "@tsconfig/node12": "^1.0.7", 23 | "@types/bn.js": "^4.11.6", 24 | "@types/jest": "^26.0.9", 25 | "@typescript-eslint/eslint-plugin": "^4.6.0", 26 | "@typescript-eslint/parser": "^4.6.0", 27 | "babel-eslint": "^10.0.3", 28 | "cross-env": "^7.0.2", 29 | "eslint": "^7.6.0", 30 | "eslint-config-prettier": "^6.11.0", 31 | "jest": "^26.4.0", 32 | "npm-run-all": "^4.1.5", 33 | "prettier": "^2.0.5", 34 | "ts-jest": "^26.2.0", 35 | "typedoc": "^0.20.36", 36 | "typescript": "^4.0.5" 37 | }, 38 | "files": [ 39 | "lib" 40 | ], 41 | "prettier": { 42 | "singleQuote": true, 43 | "trailingComma": "all" 44 | }, 45 | "dependencies": { 46 | "@project-serum/anchor": "^0.5.1-beta.2", 47 | "@project-serum/serum": "^0.13.34", 48 | "@solana/spl-token": "^0.1.3", 49 | "@solana/spl-token-registry": "^0.2.68", 50 | "@solana/web3.js": "^1.2.0", 51 | "base64-js": "^1.5.1", 52 | "bn.js": "^5.1.2" 53 | }, 54 | "browserslist": [ 55 | ">0.2%", 56 | "not dead", 57 | "not op_mini all", 58 | "maintained node versions" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /packages/swap/src/idl.ts: -------------------------------------------------------------------------------- 1 | import { Idl } from '@project-serum/anchor'; 2 | 3 | // Idl for client generation. 4 | export const IDL: Idl = { 5 | version: '0.0.0', 6 | name: 'swap', 7 | instructions: [ 8 | { 9 | name: 'swap', 10 | accounts: [ 11 | { 12 | name: 'market', 13 | accounts: [ 14 | { 15 | name: 'market', 16 | isMut: true, 17 | isSigner: false, 18 | }, 19 | { 20 | name: 'openOrders', 21 | isMut: true, 22 | isSigner: false, 23 | }, 24 | { 25 | name: 'requestQueue', 26 | isMut: true, 27 | isSigner: false, 28 | }, 29 | { 30 | name: 'eventQueue', 31 | isMut: true, 32 | isSigner: false, 33 | }, 34 | { 35 | name: 'bids', 36 | isMut: true, 37 | isSigner: false, 38 | }, 39 | { 40 | name: 'asks', 41 | isMut: true, 42 | isSigner: false, 43 | }, 44 | { 45 | name: 'orderPayerTokenAccount', 46 | isMut: true, 47 | isSigner: false, 48 | }, 49 | { 50 | name: 'coinVault', 51 | isMut: true, 52 | isSigner: false, 53 | }, 54 | { 55 | name: 'pcVault', 56 | isMut: true, 57 | isSigner: false, 58 | }, 59 | { 60 | name: 'vaultSigner', 61 | isMut: false, 62 | isSigner: false, 63 | }, 64 | { 65 | name: 'coinWallet', 66 | isMut: true, 67 | isSigner: false, 68 | }, 69 | ], 70 | }, 71 | { 72 | name: 'authority', 73 | isMut: false, 74 | isSigner: true, 75 | }, 76 | { 77 | name: 'pcWallet', 78 | isMut: true, 79 | isSigner: false, 80 | }, 81 | { 82 | name: 'dexProgram', 83 | isMut: false, 84 | isSigner: false, 85 | }, 86 | { 87 | name: 'tokenProgram', 88 | isMut: false, 89 | isSigner: false, 90 | }, 91 | { 92 | name: 'rent', 93 | isMut: false, 94 | isSigner: false, 95 | }, 96 | ], 97 | args: [ 98 | { 99 | name: 'side', 100 | type: { 101 | defined: 'Side', 102 | }, 103 | }, 104 | { 105 | name: 'amount', 106 | type: 'u64', 107 | }, 108 | { 109 | name: 'minExpectedSwapAmount', 110 | type: 'u64', 111 | }, 112 | ], 113 | }, 114 | { 115 | name: 'swapTransitive', 116 | accounts: [ 117 | { 118 | name: 'from', 119 | accounts: [ 120 | { 121 | name: 'market', 122 | isMut: true, 123 | isSigner: false, 124 | }, 125 | { 126 | name: 'openOrders', 127 | isMut: true, 128 | isSigner: false, 129 | }, 130 | { 131 | name: 'requestQueue', 132 | isMut: true, 133 | isSigner: false, 134 | }, 135 | { 136 | name: 'eventQueue', 137 | isMut: true, 138 | isSigner: false, 139 | }, 140 | { 141 | name: 'bids', 142 | isMut: true, 143 | isSigner: false, 144 | }, 145 | { 146 | name: 'asks', 147 | isMut: true, 148 | isSigner: false, 149 | }, 150 | { 151 | name: 'orderPayerTokenAccount', 152 | isMut: true, 153 | isSigner: false, 154 | }, 155 | { 156 | name: 'coinVault', 157 | isMut: true, 158 | isSigner: false, 159 | }, 160 | { 161 | name: 'pcVault', 162 | isMut: true, 163 | isSigner: false, 164 | }, 165 | { 166 | name: 'vaultSigner', 167 | isMut: false, 168 | isSigner: false, 169 | }, 170 | { 171 | name: 'coinWallet', 172 | isMut: true, 173 | isSigner: false, 174 | }, 175 | ], 176 | }, 177 | { 178 | name: 'to', 179 | accounts: [ 180 | { 181 | name: 'market', 182 | isMut: true, 183 | isSigner: false, 184 | }, 185 | { 186 | name: 'openOrders', 187 | isMut: true, 188 | isSigner: false, 189 | }, 190 | { 191 | name: 'requestQueue', 192 | isMut: true, 193 | isSigner: false, 194 | }, 195 | { 196 | name: 'eventQueue', 197 | isMut: true, 198 | isSigner: false, 199 | }, 200 | { 201 | name: 'bids', 202 | isMut: true, 203 | isSigner: false, 204 | }, 205 | { 206 | name: 'asks', 207 | isMut: true, 208 | isSigner: false, 209 | }, 210 | { 211 | name: 'orderPayerTokenAccount', 212 | isMut: true, 213 | isSigner: false, 214 | }, 215 | { 216 | name: 'coinVault', 217 | isMut: true, 218 | isSigner: false, 219 | }, 220 | { 221 | name: 'pcVault', 222 | isMut: true, 223 | isSigner: false, 224 | }, 225 | { 226 | name: 'vaultSigner', 227 | isMut: false, 228 | isSigner: false, 229 | }, 230 | { 231 | name: 'coinWallet', 232 | isMut: true, 233 | isSigner: false, 234 | }, 235 | ], 236 | }, 237 | { 238 | name: 'authority', 239 | isMut: false, 240 | isSigner: true, 241 | }, 242 | { 243 | name: 'pcWallet', 244 | isMut: true, 245 | isSigner: false, 246 | }, 247 | { 248 | name: 'dexProgram', 249 | isMut: false, 250 | isSigner: false, 251 | }, 252 | { 253 | name: 'tokenProgram', 254 | isMut: false, 255 | isSigner: false, 256 | }, 257 | { 258 | name: 'rent', 259 | isMut: false, 260 | isSigner: false, 261 | }, 262 | ], 263 | args: [ 264 | { 265 | name: 'amount', 266 | type: 'u64', 267 | }, 268 | { 269 | name: 'minExpectedSwapAmount', 270 | type: 'u64', 271 | }, 272 | ], 273 | }, 274 | ], 275 | types: [ 276 | { 277 | name: 'Side', 278 | type: { 279 | kind: 'enum', 280 | variants: [ 281 | { 282 | name: 'Bid', 283 | }, 284 | { 285 | name: 'Ask', 286 | }, 287 | ], 288 | }, 289 | }, 290 | ], 291 | events: [ 292 | { 293 | name: 'DidSwap', 294 | fields: [ 295 | { 296 | name: 'given_amount', 297 | type: 'u64', 298 | index: false, 299 | }, 300 | { 301 | name: 'min_expected_swap_amount', 302 | type: 'u64', 303 | index: false, 304 | }, 305 | { 306 | name: 'from_amount', 307 | type: 'u64', 308 | index: false, 309 | }, 310 | { 311 | name: 'to_amount', 312 | type: 'u64', 313 | index: false, 314 | }, 315 | { 316 | name: 'spill_amount', 317 | type: 'u64', 318 | index: false, 319 | }, 320 | { 321 | name: 'from_mint', 322 | type: 'publicKey', 323 | index: false, 324 | }, 325 | { 326 | name: 'to_mint', 327 | type: 'publicKey', 328 | index: false, 329 | }, 330 | { 331 | name: 'quote_mint', 332 | type: 'publicKey', 333 | index: false, 334 | }, 335 | { 336 | name: 'authority', 337 | type: 'publicKey', 338 | index: false, 339 | }, 340 | ], 341 | }, 342 | ], 343 | errors: [ 344 | { 345 | code: 100, 346 | name: 'SwapTokensCannotMatch', 347 | msg: 'The tokens being swapped must have different mints', 348 | }, 349 | { 350 | code: 101, 351 | name: 'SlippageExceeded', 352 | msg: 'Slippage tolerance exceeded', 353 | }, 354 | ], 355 | }; 356 | -------------------------------------------------------------------------------- /packages/swap/src/swap-markets.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@project-serum/anchor'; 2 | import { OpenOrders } from '@project-serum/serum'; 3 | import { TokenListContainer } from '@solana/spl-token-registry'; 4 | import { PublicKey } from '@solana/web3.js'; 5 | import { DEX_PID, USDC_PUBKEY, USDT_PUBKEY } from './utils'; 6 | 7 | // Utility class to parse the token list for markets. 8 | export default class SwapMarkets { 9 | constructor( 10 | private provider: Provider, 11 | private tokenList: TokenListContainer, 12 | ) {} 13 | 14 | public tokens(): PublicKey[] { 15 | return this.tokenList 16 | .getList() 17 | .filter((t) => { 18 | const isUsdxQuoted = 19 | t.extensions?.serumV3Usdt || t.extensions?.serumV3Usdc; 20 | return isUsdxQuoted; 21 | }) 22 | .map((t) => new PublicKey(t.address)); 23 | } 24 | 25 | public pairs(mint: PublicKey): PublicKey[] { 26 | const tokenList = this.tokenList.getList(); 27 | 28 | const mintInfo = this.tokenList 29 | .getList() 30 | .filter((t) => t.address === mint.toString())[0]; 31 | if (mintInfo === undefined) { 32 | return []; 33 | } 34 | const pairs = new Set(); 35 | 36 | // Add all tokens that also have USDC quoted markets. 37 | if (mintInfo.extensions?.serumV3Usdc) { 38 | pairs.add(USDC_PUBKEY.toString()); 39 | let iter = tokenList 40 | .filter( 41 | (t) => t.address !== mintInfo.address && t.extensions?.serumV3Usdc, 42 | ) 43 | .map((t) => t.address); 44 | iter.forEach(pairs.add, pairs); 45 | } 46 | 47 | // Add all tokens that also have USDT quoted markets. 48 | if (mintInfo.extensions?.serumV3Usdt) { 49 | pairs.add(USDT_PUBKEY.toString()); 50 | tokenList 51 | .filter( 52 | (t) => t.address !== mintInfo.address && t.extensions?.serumV3Usdt, 53 | ) 54 | .map((t) => t.address) 55 | .forEach(pairs.add, pairs); 56 | } 57 | 58 | return [...pairs].map((t) => new PublicKey(t)); 59 | } 60 | 61 | // Returns the `usdxMint` quoted market address *if* no open orders account 62 | // already exists. 63 | public async getMarketAddressIfNeeded( 64 | usdxMint: PublicKey, 65 | baseMint: PublicKey, 66 | ): Promise { 67 | const marketAddress = this.getMarketAddress(usdxMint, baseMint); 68 | let accounts = await OpenOrders.findForMarketAndOwner( 69 | this.provider.connection, 70 | marketAddress, 71 | this.provider.wallet.publicKey, 72 | DEX_PID, 73 | ); 74 | if (accounts[0] !== undefined) { 75 | throw new Error('Open orders account already exists'); 76 | } 77 | return marketAddress; 78 | } 79 | 80 | // Returns the `usdxMint` quoted market address. 81 | public getMarketAddress(usdxMint: PublicKey, baseMint: PublicKey): PublicKey { 82 | const market = this.tokenList 83 | .getList() 84 | .filter((t) => { 85 | if (t.address !== baseMint?.toString()) { 86 | return false; 87 | } 88 | if (usdxMint.equals(USDC_PUBKEY)) { 89 | return t.extensions?.serumV3Usdc !== undefined; 90 | } else if (usdxMint.equals(USDT_PUBKEY)) { 91 | return t.extensions?.serumV3Usdt !== undefined; 92 | } else { 93 | return false; 94 | } 95 | }) 96 | .map((t) => { 97 | if (usdxMint!.equals(USDC_PUBKEY)) { 98 | return new PublicKey(t.extensions!.serumV3Usdc as string); 99 | } else { 100 | return new PublicKey(t.extensions!.serumV3Usdt as string); 101 | } 102 | })[0]; 103 | if (market === undefined) { 104 | throw new Error( 105 | `Usd(x) quoted market not found for ${baseMint.toString()}`, 106 | ); 107 | } 108 | return market; 109 | } 110 | 111 | // Returns true if there's a trade across two USDC quoted markets 112 | // `fromMint` `toMint`. 113 | public usdcPathExists(fromMint: PublicKey, toMint: PublicKey): boolean { 114 | const fromMarket = this.tokenList 115 | .getList() 116 | .filter((t) => t.address === fromMint.toString()) 117 | .filter((t) => t.extensions?.serumV3Usdc !== undefined)[0]; 118 | const toMarket = this.tokenList 119 | .getList() 120 | .filter((t) => t.address === toMint.toString()) 121 | .filter((t) => t.extensions?.serumV3Usdc !== undefined)[0]; 122 | return fromMarket !== undefined && toMarket !== undefined; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /packages/swap/src/utils.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | 4 | // Serum DEX program id on mainnet-beta. 5 | export const DEX_PID = new PublicKey( 6 | '9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin', 7 | ); 8 | 9 | // Swap program id on mainnet-beta. 10 | export const SWAP_PID = new PublicKey( 11 | '22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD', 12 | ); 13 | 14 | // USDC mint on mainnet-beta. 15 | export const USDC_PUBKEY = new PublicKey( 16 | 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 17 | ); 18 | 19 | // USDT mint on mainnet-beta. 20 | export const USDT_PUBKEY = new PublicKey( 21 | 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', 22 | ); 23 | 24 | // Return the program derived address used by the serum DEX to control token 25 | // vaults. 26 | export async function getVaultOwnerAndNonce( 27 | marketPublicKey: PublicKey, 28 | dexProgramId: PublicKey = DEX_PID, 29 | ) { 30 | const nonce = new BN(0); 31 | while (nonce.toNumber() < 255) { 32 | try { 33 | const vaultOwner = await PublicKey.createProgramAddress( 34 | [marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, 'le', 8)], 35 | dexProgramId, 36 | ); 37 | return [vaultOwner, nonce]; 38 | } catch (e) { 39 | nonce.iaddn(1); 40 | } 41 | } 42 | throw new Error('Unable to find nonce'); 43 | } 44 | 45 | // Returns an associated token address for spl tokens. 46 | export async function getAssociatedTokenAddress( 47 | associatedProgramId: PublicKey, 48 | programId: PublicKey, 49 | mint: PublicKey, 50 | owner: PublicKey, 51 | ): Promise { 52 | return ( 53 | await PublicKey.findProgramAddress( 54 | [owner.toBuffer(), programId.toBuffer(), mint.toBuffer()], 55 | associatedProgramId, 56 | ) 57 | )[0]; 58 | } 59 | -------------------------------------------------------------------------------- /packages/swap/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node12/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "noImplicitAny": false, 10 | "resolveJsonModule": true, 11 | "sourceMap": true 12 | }, 13 | "include": ["./src/**/*"], 14 | "exclude": ["./src/**/*.test.js", "node_modules", "**/node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/token/README.md: -------------------------------------------------------------------------------- 1 | Utilities for interacting with SPL Tokens. 2 | 3 | [API Reference](https://project-serum.github.io/serum-ts/token/modules/_index_.html) 4 | -------------------------------------------------------------------------------- /packages/token/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-serum/token", 3 | "version": "0.1.0", 4 | "description": "Utilities for interacting with SPL Tokens", 5 | "repository": "project-serum/serum-ts", 6 | "main": "dist/lib/index.js", 7 | "types": "dist/lib/index.d.ts", 8 | "exports": { 9 | ".": "./dist/lib/index.js" 10 | }, 11 | "license": "Apache-2.0", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "engines": { 16 | "node": ">=10" 17 | }, 18 | "scripts": { 19 | "build": "tsc", 20 | "docs": "typedoc --out ../../docs/token --mode library --composite false --rootDir src src/index.ts src/*.d.ts", 21 | "start": "tsc --watch", 22 | "test": "", 23 | "clean": "rm -rf dist", 24 | "prepare": "run-s clean build" 25 | }, 26 | "dependencies": { 27 | "@project-serum/borsh": "^0.0.1-beta.0", 28 | "bn.js": "^5.1.3" 29 | }, 30 | "peerDependencies": { 31 | "@solana/web3.js": "^1.17.0" 32 | }, 33 | "files": [ 34 | "dist" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/token/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './instructions'; 2 | export * from './state'; 3 | export * as metadata from './metadata'; 4 | -------------------------------------------------------------------------------- /packages/token/src/metadata.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { rustEnum, bool, u8, publicKey, str, vec, option, struct } from '@project-serum/borsh' 3 | import { u16 } from 'buffer-layout'; 4 | 5 | const KEY_LAYOUT = rustEnum([ 6 | struct([], 'uninitialized'), 7 | struct([], 'editionV1'), 8 | struct([], 'masterEditionV1'), 9 | struct([], 'reservationListV1'), 10 | struct([], 'metadataV1'), 11 | struct([], 'reservationListV2'), 12 | struct([], 'masterEditionV2'), 13 | struct([], 'editionMarker'), 14 | ]); 15 | 16 | const CREATOR_LAYOUT = struct([ 17 | publicKey('address'), 18 | bool('verified'), 19 | u8('share'), 20 | ]) 21 | 22 | const DATA_LAYOUT = struct([ 23 | str('name'), 24 | str('symbol'), 25 | str('uri'), 26 | u16('sellerFeeBasisPoints'), 27 | option(vec(CREATOR_LAYOUT.replicate('creators')), 'creators') 28 | ]) 29 | 30 | const METADATA_LAYOUT = struct([ 31 | KEY_LAYOUT.replicate('key'), 32 | publicKey('updateAuthority'), 33 | publicKey('mint'), 34 | DATA_LAYOUT.replicate('data'), 35 | bool('primarySaleHappened'), 36 | bool('isMutable'), 37 | option(u8(), 'editionNonce'), 38 | ]); 39 | 40 | export interface Metadata { 41 | key: Key; 42 | updateAuthority: PublicKey; 43 | mint: PublicKey; 44 | data: Data; 45 | primarySaleHappened: boolean; 46 | isMutable: boolean; 47 | editionNonce: number; 48 | } 49 | 50 | export interface Data { 51 | name: string; 52 | symbol: string; 53 | uri: string; 54 | sellerFeeBasisPoints: number; 55 | creators: Array | null; 56 | } 57 | 58 | export interface Creator { 59 | address: PublicKey; 60 | verified: boolean; 61 | share: number; 62 | } 63 | 64 | export type Key = 65 | { unitialized: {} } 66 | | { editionV1: {} } 67 | | { masterEditionV1: {} } 68 | | { reserverationListV1: {} } 69 | | { metadataV1: {} } 70 | | { reservationListV2: {} } 71 | | { masterEditionV2: {} } 72 | | { editoinMarket: {} }; 73 | 74 | // eslint-disable-next-line no-control-regex 75 | const METADATA_REPLACE = new RegExp('\u0000', 'g'); 76 | 77 | export function decodeMetadata(buffer: Buffer): Metadata { 78 | const metadata: any = METADATA_LAYOUT.decode(buffer); 79 | metadata.data.name = metadata.data.name.replace(METADATA_REPLACE, ''); 80 | metadata.data.uri = metadata.data.uri.replace(METADATA_REPLACE, ''); 81 | metadata.data.symbol = metadata.data.symbol.replace(METADATA_REPLACE, ''); 82 | return metadata; 83 | }; 84 | -------------------------------------------------------------------------------- /packages/token/src/state.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import BN from 'bn.js'; 3 | import { bool, Layout, option, publicKey, u64 } from '@project-serum/borsh'; 4 | import { struct, u8 } from 'buffer-layout'; 5 | 6 | export interface Mint { 7 | mintAuthority: PublicKey | null; 8 | supply: BN; 9 | decimals: number; 10 | initialized: boolean; 11 | freezeAuthority: PublicKey | null; 12 | } 13 | 14 | export interface TokenAccount { 15 | mint: PublicKey; 16 | owner: PublicKey; 17 | amount: BN; 18 | delegate: PublicKey | null; 19 | state: number; 20 | native: BN | null; 21 | delegatedAmount: BN; 22 | closeAuthority: PublicKey | null; 23 | } 24 | 25 | export const Mint: Layout = struct([ 26 | option(publicKey(), 'mintAuthority'), 27 | u64('supply'), 28 | u8('decimals'), 29 | bool('initialized'), 30 | option(publicKey(), 'freezeAuthority'), 31 | ]); 32 | 33 | export const TokenAccount: Layout = struct([ 34 | publicKey('mint'), 35 | publicKey('owner'), 36 | u64('amount'), 37 | option(publicKey(), 'delegate'), 38 | u8('state'), 39 | option(u64(), 'delegatedAmount'), 40 | option(publicKey(), 'closeAuthority'), 41 | ]); 42 | 43 | export function decodeMintAccountData(data: Buffer): Mint { 44 | return Mint.decode(data); 45 | } 46 | 47 | export function decodeTokenAccountData(data: Buffer): TokenAccount { 48 | return TokenAccount.decode(data); 49 | } 50 | -------------------------------------------------------------------------------- /packages/token/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | 6 | "outDir": "./dist/lib", 7 | "rootDir": "./src", 8 | 9 | "composite": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | 14 | "strict": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "typeRoots": ["../../types/", "../../node_modules/@types"] 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["src/**/*.test.ts", "**/node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/tokens/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-serum/tokens", 3 | "version": "0.0.7", 4 | "description": "Serum tokens", 5 | "main": "dist/index.umd.js", 6 | "module": "dist/index.es5.js", 7 | "types": "dist/lib/src/index.d.ts", 8 | "license": "Apache-2.0", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "build": "yarn build:node && yarn build:browser", 14 | "build:node": "tsc -b && rollup -c rollup/rollup.config.ts", 15 | "build:browser": "tsc -b && rollup -c rollup/rollup.config.browser.ts", 16 | "coverage": "jest --coverage test", 17 | "prepublishOnly": "yarn build" 18 | }, 19 | "jest": { 20 | "transform": { 21 | ".(ts)": "ts-jest" 22 | }, 23 | "testEnvironment": "node", 24 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts)$", 25 | "moduleFileExtensions": [ 26 | "ts", 27 | "tsx", 28 | "js" 29 | ] 30 | }, 31 | "browser": { 32 | "./dist/index.umd.js": "./dist/index.browser.umd.js", 33 | "./dist/index.es5.js": "./dist/index.browser.es5.js" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/tokens/rollup/rollup.config.browser.ts: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import sourceMaps from 'rollup-plugin-sourcemaps'; 4 | import typescript from 'rollup-plugin-typescript2'; 5 | import json from 'rollup-plugin-json'; 6 | 7 | export default { 8 | input: `src/index.ts`, 9 | output: [ 10 | { 11 | file: './dist/index.browser.umd.js', 12 | name: 'index', 13 | format: 'umd', 14 | sourcemap: true, 15 | }, 16 | { 17 | file: './dist/index.browser.es5.js', 18 | format: 'es', 19 | sourcemap: true, 20 | }, 21 | ], 22 | external: [], 23 | watch: { 24 | include: 'src/**', 25 | }, 26 | plugins: [ 27 | resolve({ 28 | browser: true, 29 | }), 30 | commonjs(), 31 | json(), 32 | typescript({ useTsconfigDeclarationDir: true }), 33 | sourceMaps(), 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /packages/tokens/rollup/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import sourceMaps from 'rollup-plugin-sourcemaps'; 4 | import typescript from 'rollup-plugin-typescript2'; 5 | import json from 'rollup-plugin-json'; 6 | 7 | const pkg = require('../package.json'); 8 | 9 | export default { 10 | input: `src/index.ts`, 11 | output: [ 12 | { 13 | file: pkg.main, 14 | name: 'index', 15 | format: 'umd', 16 | sourcemap: true, 17 | }, 18 | { 19 | file: pkg.module, 20 | format: 'es', 21 | sourcemap: true, 22 | }, 23 | ], 24 | external: [], 25 | watch: { 26 | include: 'src/**', 27 | }, 28 | plugins: [ 29 | resolve(), 30 | commonjs(), 31 | json(), 32 | typescript({ useTsconfigDeclarationDir: true }), 33 | sourceMaps(), 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /packages/tokens/src/devnet.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "tokenSymbol": "SOL", 4 | "mintAddress": "So11111111111111111111111111111111111111112", 5 | "tokenName": "Solana", 6 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/solana/info/logo.png" 7 | }, 8 | { 9 | "tokenSymbol": "XYZ", 10 | "mintAddress": "DEhAasscXF4kEGxFgJ3bq4PpVGp5wyUxMRvn6TzGVHaw", 11 | "tokenName": "XYZ Test", 12 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/08d734b5e6ec95227dc50efef3a9cdfea4c398a1/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png" 13 | }, 14 | { 15 | "tokenSymbol": "ABC", 16 | "mintAddress": "6z83b76xbSm5UhdG33ePh7QCbLS8YaXCQ9up86tDTCUH", 17 | "tokenName": "ABC Test", 18 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/08d734b5e6ec95227dc50efef3a9cdfea4c398a1/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png" 19 | }, 20 | { 21 | "tokenSymbol": "DEF", 22 | "mintAddress": "3pyeDv6AV1RQuA6KzsqkZrpsNn4b3hooHrQhGs7K2TYa", 23 | "tokenName": "DEF Test", 24 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/08d734b5e6ec95227dc50efef3a9cdfea4c398a1/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /packages/tokens/src/index.ts: -------------------------------------------------------------------------------- 1 | import MAINNET_TOKENS from './mainnet-beta.json'; 2 | import DEVNET_TOKENS from './devnet.json'; 3 | import TESTNET_TOKENS from './testnet.json'; 4 | 5 | export const TOKENS = { 6 | mainnet: MAINNET_TOKENS, 7 | devnet: DEVNET_TOKENS, 8 | testnet: TESTNET_TOKENS, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/tokens/src/testnet.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "tokenSymbol": "SOL", 4 | "mintAddress": "So11111111111111111111111111111111111111112", 5 | "tokenName": "Solana", 6 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/solana/info/logo.png" 7 | }, 8 | { 9 | "tokenSymbol": "ABC", 10 | "mintAddress": "D4fdoY5d2Bn1Cmjqy6J6shRHjcs7QNuBPzwEzTLrf7jm", 11 | "tokenName": "ABC Test", 12 | "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/bitcoin/info/logo.png" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /packages/tokens/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "./src/*.json"], 4 | "compilerOptions": { 5 | "outDir": "dist/lib", 6 | "resolveJsonModule": true, 7 | "composite": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es6", 5 | "module": "es2015", 6 | "lib": ["es2015", "es2016", "es2017", "dom"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "noImplicitAny": true, 14 | "typeRoots": ["types/", "node_modules/@types"] 15 | }, 16 | "include": ["src/**/*"] 17 | } 18 | -------------------------------------------------------------------------------- /types/buffer-layout/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'buffer-layout' { 2 | // TODO: remove `any`. 3 | export class Layout { 4 | span: number; 5 | property?: string; 6 | 7 | constructor(span: number, property?: string); 8 | 9 | decode(b: Buffer, offset?: number): T; 10 | encode(src: T, b: Buffer, offset?: number): number; 11 | getSpan(b: Buffer, offset?: number): number; 12 | replicate(name: string): this; 13 | } 14 | // TODO: remove any. 15 | export class Structure extends Layout { 16 | span: any; 17 | } 18 | export function greedy( 19 | elementSpan?: number, 20 | property?: string, 21 | ): Layout; 22 | export function offset( 23 | layout: Layout, 24 | offset?: number, 25 | property?: string, 26 | ): Layout; 27 | export function u8(property?: string): Layout; 28 | export function u16(property?: string): Layout; 29 | export function u24(property?: string): Layout; 30 | export function u32(property?: string): Layout; 31 | export function u40(property?: string): Layout; 32 | export function u48(property?: string): Layout; 33 | export function nu64(property?: string): Layout; 34 | export function u16be(property?: string): Layout; 35 | export function u24be(property?: string): Layout; 36 | export function u32be(property?: string): Layout; 37 | export function u40be(property?: string): Layout; 38 | export function u48be(property?: string): Layout; 39 | export function nu64be(property?: string): Layout; 40 | export function s8(property?: string): Layout; 41 | export function s16(property?: string): Layout; 42 | export function s24(property?: string): Layout; 43 | export function s32(property?: string): Layout; 44 | export function s40(property?: string): Layout; 45 | export function s48(property?: string): Layout; 46 | export function ns64(property?: string): Layout; 47 | export function s16be(property?: string): Layout; 48 | export function s24be(property?: string): Layout; 49 | export function s32be(property?: string): Layout; 50 | export function s40be(property?: string): Layout; 51 | export function s48be(property?: string): Layout; 52 | export function ns64be(property?: string): Layout; 53 | export function f32(property?: string): Layout; 54 | export function f32be(property?: string): Layout; 55 | export function f64(property?: string): Layout; 56 | export function f64be(property?: string): Layout; 57 | export function struct( 58 | fields: Layout[], 59 | property?: string, 60 | decodePrefixes?: boolean, 61 | ): Layout; 62 | export function bits( 63 | word: Layout, 64 | msb?: boolean, 65 | property?: string, 66 | ): any; 67 | export function seq( 68 | elementLayout: Layout, 69 | count: number | Layout, 70 | property?: string, 71 | ): Layout; 72 | export function union( 73 | discr: Layout, 74 | defaultLayout?: any, 75 | property?: string, 76 | ): any; 77 | export function unionLayoutDiscriminator( 78 | layout: Layout, 79 | property?: string, 80 | ): any; 81 | export function blob( 82 | length: number | Layout, 83 | property?: string, 84 | ): Layout; 85 | export function cstr(property?: string): Layout; 86 | export function utf8(maxSpan: number, property?: string): Layout; 87 | } 88 | --------------------------------------------------------------------------------