├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── enhancement.yml └── workflows │ └── tests.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.cjs ├── docs ├── dex-transaction.md ├── models.md ├── providers │ ├── data.md │ └── wallet.md └── requests │ ├── cancel-swap-request.md │ ├── fetch-request.md │ ├── split-cancel-swap-request.md │ ├── split-swap-request.md │ └── swap-request.md ├── jest.config.cjs ├── package-lock.json ├── package.json ├── src ├── constants.ts ├── definition-builder.ts ├── dex │ ├── api │ │ ├── base-api.ts │ │ ├── minswap-api.ts │ │ ├── muesliswap-api.ts │ │ ├── splash-api.ts │ │ ├── sundaeswap-v1-api.ts │ │ ├── sundaeswap-v3-api.ts │ │ ├── vyfinance-api.ts │ │ └── wingriders-api.ts │ ├── base-dex.ts │ ├── definitions │ │ ├── minswap-v2 │ │ │ ├── order.ts │ │ │ └── pool.ts │ │ ├── minswap │ │ │ ├── order.ts │ │ │ └── pool.ts │ │ ├── muesliswap │ │ │ ├── order.ts │ │ │ └── pool.ts │ │ ├── splash │ │ │ ├── order.ts │ │ │ └── pool.ts │ │ ├── sundaeswap-v1 │ │ │ ├── order.ts │ │ │ └── pool.ts │ │ ├── sundaeswap-v3 │ │ │ ├── order.ts │ │ │ └── pool.ts │ │ ├── vyfinance │ │ │ ├── order.ts │ │ │ └── pool.ts │ │ ├── wingriders-v2 │ │ │ ├── order.ts │ │ │ └── pool.ts │ │ └── wingriders │ │ │ ├── order.ts │ │ │ └── pool.ts │ ├── logo │ │ ├── minswap.png │ │ ├── minswapv2.png │ │ ├── muesliswap.png │ │ ├── splash.png │ │ ├── sundaeswap.png │ │ ├── vyfinance.png │ │ ├── wingriders.png │ │ └── wingridersv2.png │ ├── minswap-v2.ts │ ├── minswap.ts │ ├── models │ │ ├── asset.ts │ │ ├── dex-transaction.ts │ │ └── liquidity-pool.ts │ ├── muesliswap.ts │ ├── splash.ts │ ├── sundaeswap-v1.ts │ ├── sundaeswap-v3.ts │ ├── vyfinance.ts │ ├── wingriders-v2.ts │ └── wingriders.ts ├── dexter.ts ├── index.ts ├── providers │ ├── asset-metadata │ │ ├── base-metadata-provider.ts │ │ └── token-registry-provider.ts │ ├── data │ │ ├── base-data-provider.ts │ │ ├── blockfrost-provider.ts │ │ ├── kupo-provider.ts │ │ └── mock-data-provider.ts │ └── wallet │ │ ├── base-wallet-provider.ts │ │ ├── lucid-provider.ts │ │ └── mock-wallet-provider.ts ├── requests │ ├── cancel-swap-request.ts │ ├── fetch-request.ts │ ├── split-cancel-swap-request.ts │ ├── split-swap-request.ts │ └── swap-request.ts ├── types.ts └── utils.ts ├── tests ├── dex-transaction.test.ts ├── minswap.test.ts ├── muesliswap.test.ts ├── split-swap-request.test.ts ├── sundaeswap-v1.test.ts ├── swap-request.test.ts ├── vyfinance.test.ts └── wingriders.test.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: dropdown 11 | id: data-provider 12 | attributes: 13 | label: Data Provider 14 | description: Which Data Provider you were using 15 | options: 16 | - Blockfrost 17 | - Kupo 18 | - Custom 19 | validations: 20 | required: true 21 | - type: dropdown 22 | id: dex 23 | attributes: 24 | label: DEX 25 | description: Which DEX this bug occurred for 26 | options: 27 | - All 28 | - Minswap 29 | - SundaeSwap 30 | - MuesliSwap 31 | - WingRiders 32 | - VyFinance 33 | validations: 34 | required: true 35 | - type: textarea 36 | id: description 37 | attributes: 38 | label: Description 39 | description: Give context for the bug 40 | placeholder: What happened? 41 | validations: 42 | required: true 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.yml: -------------------------------------------------------------------------------- 1 | name: Enhancement 2 | description: Suggest an Enhancement 3 | title: "[Enhancement]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to help us improve Dexter! 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Description 14 | description: Give context for your idea 15 | validations: 16 | required: true 17 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Test using Node.js 11 | uses: actions/setup-node@v3 12 | with: 13 | node-version: '17' 14 | - run: npm ci 15 | - run: npm run test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | .idea/ 4 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tests 3 | tsconfig.json 4 | babel.config.cjs 5 | jest.config.cjs -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to Dexter will be documented in this file. 4 | 5 | ## [v5.4.9] 6 | - Splash integration 7 | 8 | ## [v5.4.0] 9 | - SundaeSwap v3 integration 10 | 11 | ## [v5.3.0] 12 | - Minswap v2 integration 13 | 14 | ## [v5.2.0] 15 | - Add `withMinimumReceive(minReceive: bigint)` to SwapRequest 16 | 17 | ## [v5.1.0] 18 | - Fix cancelling orders for each DEX 19 | - Add new split cancel order request 20 | 21 | ## [v5.0.0] 22 | - TeddySwap integration 23 | - Spectrum integration 24 | 25 | ## [v4.2.0] 26 | - Fix WR price impact formula for 0 decimals 27 | - Rename Asset identifier function 28 | - Include '/' helper function for proxy URLs 29 | - Add export for SplitSwapRequest 30 | - Add tests for DexTransaction events 31 | - Fix `withSwapOutAmountMappings` for split swap requests 32 | - Add fetching for total LP tokens for liquidity pools 33 | 34 | ## [v4.1.0] 35 | - Support for multi-dex swap requests. 36 | 37 | ## [v4.0.2] 38 | - Fix pool identifiers & LP token for Minswap. 39 | 40 | ## [v4.0.1] 41 | - Remove total LP tokens from fetched data. This data is not needed for swapping, and wastes a lot of network requests. 42 | - Add `setProviderForDex()` to use different data providers for each DEX. 43 | 44 | ## [v3.0.3] 45 | - Fix for Minswap calculations with pool fee percents to round before casting. 46 | 47 | ## [v3.0.2] 48 | - Update DEX template definitions to use a copy of the template, rather than altering the original. 49 | - Fix for WingRiders API. 50 | 51 | ## [v3.0.1] 52 | 53 | - Fix for WingRiders price impact calculation when using a non ADA swap in token. 54 | - Expose address payments in `DexTransaction` instance. 55 | - Update DEX `name` variable to `identifier` to resolve browser related issue with reserved words. 56 | 57 | ## [v2.0.0] 58 | 59 | - Adjust Kupo & Blockfrost data providers to accept an optional `RequestConfig`. 60 | - Cleanup around asset filtering when using `FetchRequest.getLiquidityPools()`. 61 | - Add `FetchRequest.getLiquidityPoolState()` helper to get the latest state for a liquidity pool. 62 | - Liquidity pool fee fix for SundaeSwap when constructing pools from on-chain data. 63 | - Add ability to retry API requests in the `RequestConfig` options. 64 | - Add handling for Blockfrost API limit cooldown. 65 | - Add `SwapRequest.withSwapOutAmount(bigint)` to calculate the estimated swap in amount. 66 | - Add `SwapRequest.withSwapOutToken(Token)` to allow crafting a SwapRequest given the swap out token. 67 | - Update `FetchRequest.forDexs()` to `FetchRequest.onDexs()`. 68 | - Update `FetchRequest.forAllDexs()` to `FetchRequest.onAllDexs()`. 69 | - Add `FetchRequest.forTokens()` & `FetchRequest.forTokenPairs()` for filtering pools containing tokens/token pairs. 70 | - Fix for encrypted Minswap API responses (API still has hard call limits). 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Indigo Protocol 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/preset-typescript", 12 | ] 13 | }; -------------------------------------------------------------------------------- /docs/dex-transaction.md: -------------------------------------------------------------------------------- 1 |

2 |

DEX Transactions

3 |

4 | 5 | You should never have to create DEX transactions yourself. However, the documentation below gives a rundown 6 | on how you can obtain information for your swap transactions, as well as listen for specific events through the Tx lifecycle. 7 | 8 | ### Getters 9 | 10 |
11 | hash(): string Get the Tx hash if available 12 | 13 | ##### Using 14 | 15 | ```js 16 | console.log(transaction.hash); 17 | ``` 18 |
19 | 20 |
21 | 22 |
23 | isSigned(): boolean Get whether the Tx has been signed 24 | 25 | ##### Using 26 | 27 | ```js 28 | console.log(transaction.isSigned); 29 | ``` 30 |
31 | 32 |
33 | 34 |
35 | payments(): PayToAddress[] Get the address payments made for the Tx 36 | 37 | ##### Using 38 | 39 | ```js 40 | console.log(transaction.payments); 41 | ``` 42 |
43 | 44 |
45 | 46 |
47 | status(): TransactionStatus Get the current status of the transaction 48 | 49 | ##### Using 50 | 51 | ```js 52 | console.log(transaction.status); 53 | ``` 54 |
55 | 56 | ### Listen for Events 57 | 58 |
59 | onBuilding(TransactionCallback): DexTransaction Tx is in building status 60 | 61 | ##### Using 62 | 63 | ```js 64 | transaction.onBuilding(() => { 65 | console.log('Tx building'); 66 | }); 67 | ``` 68 |
69 | 70 |
71 | 72 |
73 | onSigning(TransactionCallback): DexTransaction Tx is in signing status 74 | 75 | ##### Using 76 | 77 | ```js 78 | transaction.onSigning(() => { 79 | console.log('Tx signing'); 80 | }); 81 | ``` 82 |
83 | 84 |
85 | 86 |
87 | onSubmitting(TransactionCallback): DexTransaction Tx is in submitting status 88 | 89 | ##### Using 90 | 91 | ```js 92 | transaction.onSubmitting(() => { 93 | console.log('Tx submitting to chain'); 94 | }); 95 | ``` 96 |
97 | 98 |
99 | 100 |
101 | onSubmitted(TransactionCallback): DexTransaction Tx has been submitted on-chain 102 | 103 | ##### Using 104 | 105 | ```js 106 | transaction.onSubmitted(() => { 107 | console.log('Tx submitted'); 108 | }); 109 | ``` 110 |
111 | 112 |
113 | 114 |
115 | onError(TransactionCallback): DexTransaction Error has occurred with the transaction 116 | 117 | ##### Using 118 | 119 | ```js 120 | transaction.onError(() => { 121 | console.log('Something went wrong'); 122 | }); 123 | ``` 124 |
125 | 126 |
127 | 128 |
129 | onFinally(TransactionCallback): DexTransaction Everything went OK or error has occurred 130 | 131 | ##### Using 132 | 133 | ```js 134 | transaction.onFinally(() => { 135 | console.log('All complete or has errored'); 136 | }); 137 | ``` 138 |
-------------------------------------------------------------------------------- /docs/models.md: -------------------------------------------------------------------------------- 1 |

2 |

Return Models

3 |

4 | 5 | Below are commonly returned models & types for your requests, along with example data. 6 | 7 | ### Asset 8 | ```js 9 | { 10 | policyId: string // '533bb94a8850ee3c...' 11 | nameHex: Token // '494e4459' 12 | decimals: Token // 6 13 | } 14 | ``` 15 | 16 | ### LiquidityPool 17 | ```js 18 | { 19 | dex: string // Minswap 20 | assetA: Token // Asset || 'lovelace' 21 | assetB: Token // Asset || 'lovelace' 22 | reserveA: bigint // 1234_000000n 23 | reserveB: bigint // 5678_000000n 24 | address: string // 'addr1LiquidityPoolAddress...' 25 | marketOrderAddress: string // 'addr1...' 26 | limitOrderAddress: string // 'addr1...' 27 | } 28 | 29 | ``` 30 | ### SwapFee 31 | ```js 32 | { 33 | id: string // 'batcherFee' 34 | title: string // 'Batcher Fee' 35 | description: string // 'Fee paid for the service of off-chain Laminar batcher to process transactions.' 36 | value: bigint // 2_000000n 37 | isReturned: boolean // false 38 | } 39 | ``` 40 | 41 | ### DexTransaction 42 | See [doc](dex-transaction.md) for more information 43 | -------------------------------------------------------------------------------- /docs/providers/data.md: -------------------------------------------------------------------------------- 1 |

2 |

Data Providers

3 |

4 | 5 | ### Mock 6 | 7 | ```js 8 | const mockProvider: BaseDataProvider = new MockDataProvider(); 9 | ``` 10 | 11 | ### Blockfrost 12 | Dexter requires a lot of requests when pulling liquidity pools, so it is advised to either [host your own Blockfrost backend](https://github.com/blockfrost/blockfrost-backend-ryo), 13 | or use an API plan sufficient to handle enough requests. 14 | ```js 15 | const blockfrostProvider: BaseDataProvider = new BlockfrostProvider( 16 | { 17 | url: 'https://cardano-mainnet.blockfrost.io/api/v0', 18 | projectId: '', 19 | } 20 | ); 21 | ``` 22 | 23 | ### Kupo 24 | 25 | ```js 26 | const kupoProvider: BaseDataProvider = new KupoProvider( 27 | { 28 | url: 'http://localhost:1442', 29 | } 30 | ); 31 | ``` 32 | 33 | For the best results, the matching patterns below give the minimum required for DEX addresses 34 | to resolve within Dexter : 35 | ``` 36 | --match "de9b756719341e79785aa13c164e7fe68c189ed04d61c9876b2fe53f.4d7565736c69537761705f414d4d" 37 | --match "026a18d04a0c642759bb3d83b12e3344894e5c1c7b2aeb1a2113a570.4c" 38 | --match "13aa2accf2e1561723aa26871e071fdf32c867cff7e7d50ad470d62f.4d494e53574150" 39 | --match "addr1w9qzpelu9hn45pefc0xr4ac4kdxeswq7pndul2vuj59u8tqaxdznu" 40 | --match "{ Your Address }" 41 | ``` 42 | 43 | An example of a full Kupo run command given these patterns : 44 | ``` 45 | kupo \ 46 | --host 0.0.0.0 \ 47 | --port 1442 \ 48 | --node-socket path/to/node/socket \ 49 | --node-config path/to/node/config \ 50 | --since origin \ 51 | --defer-db-indexes \ 52 | --workdir path/to/db/directory \ 53 | --prune-utxo \ 54 | --match "de9b756719341e79785aa13c164e7fe68c189ed04d61c9876b2fe53f.4d7565736c69537761705f414d4d" \ 55 | --match "026a18d04a0c642759bb3d83b12e3344894e5c1c7b2aeb1a2113a570.4c" \ 56 | --match "13aa2accf2e1561723aa26871e071fdf32c867cff7e7d50ad470d62f.4d494e53574150" \ 57 | --match "addr1w9qzpelu9hn45pefc0xr4ac4kdxeswq7pndul2vuj59u8tqaxdznu" 58 | --match "ffcdbb9155da0602280c04d8b36efde35e3416567f9241aff0955269.4d7565736c69537761705f414d4d" \ 59 | --match "{ Your Address }" 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/providers/wallet.md: -------------------------------------------------------------------------------- 1 |

2 |

Wallet Providers

3 |

4 | 5 | ### Mock 6 | ```js 7 | const mockProvider: BaseWalletProvider = new MockWalletProvider(); 8 | ``` 9 | 10 | ### Lucid 11 | ```js 12 | const lucidProvider: BaseWalletProvider = new LucidProvider(); 13 | ``` 14 | 15 | ##### Lucid Blockfrost 16 | ```js 17 | lucidProvider.loadWallet(walletApi, { 18 | url: 'https://cardano-mainnet.blockfrost.io/api/v0', 19 | projectId: '', 20 | }) 21 | ``` 22 | or 23 | ```js 24 | lucidProvider.loadWalletFromSeedPhrase(['...'], {}, { 25 | url: 'https://cardano-mainnet.blockfrost.io/api/v0', 26 | projectId: '', 27 | }) 28 | ``` 29 | 30 | ##### Lucid Kupmios 31 | ```js 32 | lucidProvider.loadWallet(walletApi, {}, { 33 | kupoUrl: 'http://localhost:1442', 34 | ogmiosUrl: 'ws://localhost:1337', 35 | }) 36 | ``` 37 | or 38 | ```js 39 | lucidProvider.loadWalletFromSeedPhrase(['...'], {}, { 40 | kupoUrl: 'http://localhost:1442', 41 | ogmiosUrl: 'ws://localhost:1337', 42 | }) 43 | ``` -------------------------------------------------------------------------------- /docs/requests/cancel-swap-request.md: -------------------------------------------------------------------------------- 1 |

2 |

Cancel Swap Request

3 |

4 | 5 | ### Obtaining 6 | ```js 7 | dexter.newCancelSwapRequest() 8 | ... 9 | ``` 10 | 11 | ### CancelSwapRequest API 12 | 13 |
14 | forTransaction(string): CancelSwapRequest Set which transaction to cancel from, given the Tx hash. 15 | 16 | ##### Using 17 | 18 | ```js 19 | dexter.newCancelSwapRequest() 20 | .forTransaction('abc...') 21 | ... 22 | ``` 23 |
24 | 25 |
26 | 27 |
28 | forDex(string): CancelSwapRequest Set which DEX the transaction came from. 29 | 30 | ##### Using 31 | 32 | ```js 33 | dexter.newCancelSwapRequest() 34 | .forDex(VyFinance.identifier) 35 | ... 36 | ``` 37 |
38 | 39 |
40 | 41 |
42 | cancel(): DexTransaction Submit the transaction to cancel your order. 43 | 44 | Transaction & DEX must be set beforehand. 45 | 46 | ##### Using 47 | 48 | ```js 49 | dexter.newCancelSwapRequest() 50 | ... 51 | .cancel() 52 | ``` 53 |
-------------------------------------------------------------------------------- /docs/requests/fetch-request.md: -------------------------------------------------------------------------------- 1 |

2 |

Fetch Request

3 |

4 | 5 | ### Obtaining 6 | ```js 7 | dexter.newFetchRequest() 8 | ... 9 | ``` 10 | 11 | ### FetchRequest API 12 | Omitting the `forTokens()` & `forTokenPairs()` when constructing your request will result in all possible pools. 13 | 14 | Omitting the `forDexs()` & `forAllDexs()` when constructing your request will result in all available DEXs to be used 15 | when fetching pools. 16 | 17 |
18 | onDexs(string | string[]): FetchRequest Set which DEXs to grab information for. 19 | 20 | ##### Using 21 | 22 | ```js 23 | dexter.newFetchRequest() 24 | .onDexs(WingRiders.identifier) 25 | ... 26 | ``` 27 | or 28 | ```js 29 | dexter.newFetchRequest() 30 | .onDexs([WingRiders.identifier, SundaeSwap.identifier]) 31 | ... 32 | ``` 33 |
34 | 35 |
36 | 37 |
38 | onAllDexs(): FetchRequest Grab information from all available DEXs. 39 | 40 | ##### Using 41 | 42 | ```js 43 | dexter.newFetchRequest() 44 | .onAllDexs() 45 | ... 46 | ``` 47 |
48 | 49 |
50 | 51 |
52 | setDataProviderForDex(string, BaseDataProvider): FetchRequest Force a data provider for a DEX. 53 | 54 | ##### Using 55 | 56 | ```js 57 | dexter.newFetchRequest() 58 | .onAllDexs() 59 | ... 60 | ``` 61 |
62 | 63 |
64 | 65 |
66 | forTokens(Token[]): FetchRequest Set filtering tokens when fetching liquidity pools 67 | 68 | ##### Using 69 | 70 | ```js 71 | const indyAsset: Asset = new Asset('533bb94a8850ee3ccbe483106489399112b74c905342cb1792a797a0', '494e4459', 6); 72 | 73 | // Will only fetch pools containing the INDY token 74 | dexter.newFetchRequest() 75 | .forTokens([indyAsset]) 76 | ... 77 | ``` 78 |
79 | 80 |
81 | 82 |
83 | forTokenPairs(Token[][]): FetchRequest Set filtering token pairs when fetching liquidity pools 84 | 85 | ##### Using 86 | 87 | ```js 88 | const indyAsset: Asset = new Asset('533bb94a8850ee3ccbe483106489399112b74c905342cb1792a797a0', '494e4459', 6); 89 | 90 | // Will only fetch pools containing ADA & INDY assets 91 | dexter.newFetchRequest() 92 | .forTokenPairs([ 93 | ['lovelace', indyAsset], 94 | ]) 95 | ... 96 | ``` 97 |
98 | 99 |
100 | 101 |
102 | getLiquidityPools(): Promise<LiquidityPool[]> Fetch liquidity pools from your set DEXs 103 | 104 | Providing the first or first & second parameters will filter the returned pools by the assets you provide. 105 | 106 | ##### Using 107 | 108 | ```js 109 | dexter.newFetchRequest() 110 | .onAllDexs() 111 | .getLiquidityPools() 112 | .then((pools: LiquidityPool[]) => { 113 | console.log(pools); 114 | }); 115 | ``` 116 |
117 | 118 |
119 | 120 |
121 | getLiquidityPoolState(LiquidityPool): Promise<LiquidityPool> Fetch latest state for a liquidity pool 122 | 123 | ##### Using 124 | 125 | ```js 126 | dexter.newFetchRequest() 127 | .getLiquidityPoolState(liquidityPool) 128 | .then((pool: LiquidityPool) => { 129 | console.log(pool); 130 | }); 131 | ``` 132 |
133 | -------------------------------------------------------------------------------- /docs/requests/split-cancel-swap-request.md: -------------------------------------------------------------------------------- 1 |

2 |

Split Cancel Swap Request

3 |

4 | 5 | Request a split cancel order request in order to split your cancel order into chunks on different DEXs. 6 | 7 | ### Obtaining 8 | ```js 9 | dexter.newSplitCancelSwapRequest() 10 | ... 11 | ``` 12 | 13 | ### SwapRequest API 14 | 15 |
16 | forTransactions(SplitCancelSwapMapping[]): SplitSwapRequest Set which transactions to cancel. 17 | 18 | ##### Using 19 | 20 | ```js 21 | dexter.newSplitCancelSwapRequest() 22 | .forTransactions([{ txHash: '{Tx Hash}', dex: 'Minswap' }]) 23 | ... 24 | ``` 25 |
26 | 27 |
28 | 29 |
30 | submit(): DexTransaction Finally submit your split cancel orders on-chain. 31 | 32 | ##### Using 33 | 34 | ```js 35 | dexter.newSplitCancelSwapRequest() 36 | ... 37 | .submit() 38 | ``` 39 |
40 | -------------------------------------------------------------------------------- /docs/requests/split-swap-request.md: -------------------------------------------------------------------------------- 1 |

2 |

Split Swap Request

3 |

4 | 5 | Request a split order request in order to split your swap order into chunks on different DEXs. 6 | 7 | ### Obtaining 8 | ```js 9 | dexter.newSplitSwapRequest() 10 | ... 11 | ``` 12 | 13 | ### SwapRequest API 14 | 15 |
16 | withSwapInToken(Token): SplitSwapRequest Set which Token in the pool you are swapping in. 17 | 18 | ##### Using 19 | 20 | ```js 21 | dexter.newSplitSwapRequest() 22 | .withSwapInToken('lovelace') 23 | ... 24 | ``` 25 |
26 | 27 |
28 | 29 |
30 | withSwapOutToken(Token): SplitSwapRequest Set which Token in the pool you are swapping out. 31 | 32 | ##### Using 33 | 34 | ```js 35 | dexter.newSplitSwapRequest() 36 | .withSwapOutToken('lovelace') 37 | ... 38 | ``` 39 |
40 | 41 |
42 | 43 |
44 | withSwapInAmountMappings(SwapInAmountMapping[]): SplitSwapRequest Set how much you are swapping in for each DEX. 45 | 46 | ##### Using 47 | 48 | ```js 49 | dexter.newSplitSwapRequest() 50 | .withSwapInAmountMappings([ 51 | { 52 | swapInAmount: 2_000000n, 53 | liquidityPool: new LiquidityPool(Minswap.identifier, ...) 54 | }, 55 | { 56 | swapInAmount: 5_000000n, 57 | liquidityPool: new LiquidityPool(WingRiders.identifier, ...) 58 | }, 59 | ]) 60 | ... 61 | ``` 62 |
63 | 64 |
65 | 66 |
67 | withSwapOutAmountMappings(SwapOutAmountMapping[]): SplitSwapRequest Set how much you are swapping out for each DEX. 68 | 69 | ##### Using 70 | 71 | ```js 72 | dexter.newSplitSwapRequest() 73 | .withSwapOutAmountMappings([ 74 | { 75 | swapInAmount: 2_000000n, 76 | liquidityPool: new LiquidityPool(Minswap.identifier, ...) 77 | }, 78 | { 79 | swapInAmount: 5_000000n, 80 | liquidityPool: new LiquidityPool(WingRiders.identifier, ...) 81 | }, 82 | ]) 83 | ``` 84 |
85 | 86 |
87 | 88 |
89 | flip(): SplitSwapRequest Flip your swap in and swap out token. 90 | 91 | ##### Using 92 | 93 | ```js 94 | dexter.newSplitSwapRequest() 95 | .flip() 96 | ... 97 | ``` 98 |
99 | 100 |
101 | 102 |
103 | withSlippagePercent(number): SplitSwapRequest Set how much slippage you will tolerate. (Default: 1.0%) 104 | 105 | ##### Using 106 | 107 | ```js 108 | dexter.newSplitSwapRequest() 109 | .withSlippagePercent(0.5) 110 | ... 111 | ``` 112 |
113 | 114 |
115 | 116 |
117 | getEstimatedReceive(): bigint Get the total estimated receive for your swap. 118 | 119 | Will return a sum of the estimated receive for each DEX mapping. 120 | 121 | ##### Using 122 | 123 | ```js 124 | dexter.newSplitSwapRequest() 125 | .getEstimatedReceive() 126 | ``` 127 |
128 | 129 |
130 | 131 |
132 | getMinimumReceive(): bigint Get the total minimum receive for your swap. 133 | 134 | Will return a sum of the minimum receive for each DEX mapping. 135 | 136 | ##### Using 137 | 138 | ```js 139 | dexter.newSplitSwapRequest() 140 | .getMinimumReceive() 141 | ``` 142 |
143 | 144 |
145 | 146 |
147 | getAvgPriceImpactPercent(): number Get the average price impact percentage for your swap. 148 | 149 | Will return the average price impact for each swap on each DEX. 150 | 151 | ##### Using 152 | 153 | ```js 154 | dexter.newSplitSwapRequest() 155 | .getAvgPriceImpactPercent() 156 | ``` 157 |
158 | 159 |
160 | 161 |
162 | getSwapFees(): SwapFee[] Get the DEX specific fees for your swap. 163 | 164 | Will return all swap fees associated with each DEX in the swap. 165 | 166 | ##### Using 167 | 168 | ```js 169 | dexter.newSplitSwapRequest() 170 | .getSwapFees() 171 | ``` 172 |
173 | 174 |
175 | 176 |
177 | submit(): DexTransaction Finally submit your split swap on-chain. 178 | 179 | ##### Using 180 | 181 | ```js 182 | dexter.newSplitSwapRequest() 183 | ... 184 | .submit() 185 | ``` 186 |
187 | -------------------------------------------------------------------------------- /docs/requests/swap-request.md: -------------------------------------------------------------------------------- 1 |

2 |

Swap Request

3 |

4 | 5 | All calculations for your swap parameters are DEX specific. For example, the estimated receive might be different on one DEX 6 | vs another given the same values due to DEXs using different formulas for their calculations. 7 | 8 | ### Obtaining 9 | ```js 10 | dexter.newSwapRequest() 11 | ... 12 | ``` 13 | 14 | ### SwapRequest API 15 | 16 |
17 | forLiquidityPool(LiquidityPool): SwapRequest Set which Liquidity Pool to swap from. 18 | 19 | ##### Using 20 | 21 | ```js 22 | dexter.newSwapRequest() 23 | .forLiquidityPool(new LiquidityPool(...)) 24 | ... 25 | ``` 26 |
27 | 28 |
29 | 30 |
31 | withSwapInToken(Token): SwapRequest Set which Token in the pool you are swapping in. 32 | 33 | ##### Using 34 | 35 | ```js 36 | dexter.newSwapRequest() 37 | .withSwapInToken('lovelace') 38 | ... 39 | ``` 40 |
41 | 42 |
43 | 44 |
45 | withSwapOutToken(Token): SwapRequest Set which Token in the pool you are swapping out. 46 | 47 | ##### Using 48 | 49 | ```js 50 | dexter.newSwapRequest() 51 | .withSwapOutToken('lovelace') 52 | ... 53 | ``` 54 |
55 | 56 |
57 | 58 |
59 | withSwapInAmount(bigint): SwapRequest Set how much you are swapping in. 60 | 61 | ##### Using 62 | 63 | ```js 64 | dexter.newSwapRequest() 65 | .withSwapInAmount(10_000000n) 66 | ... 67 | ``` 68 |
69 | 70 |
71 | 72 |
73 | withSwapOutAmount(bigint): SwapRequest Set how much you are swapping out. 74 | 75 | ##### Using 76 | 77 | ```js 78 | dexter.newSwapRequest() 79 | .withSwapOutAmount(10_000000n) 80 | ... 81 | ``` 82 |
83 | 84 |
85 | 86 |
87 | withMinimumReceive(bigint): SwapRequest Set the minimum you want to receive. 88 | 89 | ##### Using 90 | 91 | ```js 92 | dexter.newSwapRequest() 93 | .withMinimumReceive(10_000000n) 94 | ... 95 | ``` 96 |
97 | 98 |
99 | 100 |
101 | withSlippagePercent(number): SwapRequest Set how much slippage you will tolerate. (Default: 1.0%) 102 | 103 | ##### Using 104 | 105 | ```js 106 | dexter.newSwapRequest() 107 | .withSlippagePercent(0.5) 108 | ... 109 | ``` 110 |
111 | 112 |
113 | 114 |
115 | flip(): SwapRequest Flip your swap in and swap out token. 116 | 117 | Flipping will only affect the swap in & swap out token if the swap in token was set beforehand. 118 | 119 | ##### Using 120 | 121 | ```js 122 | dexter.newSwapRequest() 123 | .flip() 124 | ... 125 | ``` 126 |
127 | 128 |
129 | 130 |
131 | getEstimatedReceive(LiquidityPool?): bigint Get the estimated receive for your swap. 132 | 133 | Supplying a liquidity pool will run against the provided pool. This is useful when getting the estimated receive for pools with the 134 | same tokens, but on different DEXs. 135 | 136 | ##### Using 137 | 138 | ```js 139 | dexter.newSwapRequest() 140 | .getEstimatedReceive() 141 | ``` 142 |
143 | 144 |
145 | 146 |
147 | getMinimumReceive(LiquidityPool?): bigint Get the minimum receive for your swap. 148 | 149 | Supplying a liquidity pool will run against the provided pool. This is useful when getting the minimum receive for pools with the 150 | same tokens, but on different DEXs. 151 | 152 | ##### Using 153 | 154 | ```js 155 | dexter.newSwapRequest() 156 | .getMinimumReceive() 157 | ``` 158 |
159 | 160 |
161 | 162 |
163 | getPriceImpactPercent(): number Get the price impact percentage for your swap. 164 | 165 | Supplying a liquidity pool will run against the provided pool. This is useful when getting the minimum receive for pools with the 166 | same tokens, but on different DEXs. 167 | 168 | ##### Using 169 | 170 | ```js 171 | dexter.newSwapRequest() 172 | .getPriceImpactPercent() 173 | ``` 174 |
175 | 176 |
177 | 178 |
179 | getSwapFees(): SwapFee[] Get the DEX specific fees for your swap. 180 | 181 | ##### Using 182 | 183 | ```js 184 | dexter.newSwapRequest() 185 | .getSwapFees() 186 | ``` 187 |
188 | 189 |
190 | 191 |
192 | submit(): DexTransaction Finally submit your swap on-chain. 193 | 194 | ##### Using 195 | 196 | ```js 197 | dexter.newSwapRequest() 198 | ... 199 | .submit() 200 | ``` 201 |
202 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/default-esm', 3 | testEnvironment: 'node', 4 | modulePathIgnorePatterns: [ 5 | '/build/', 6 | '/node_modules/', 7 | ], 8 | transform: { 9 | "^.+\\.ts?$": "babel-jest", 10 | }, 11 | roots: [''], 12 | modulePaths: [''], 13 | moduleDirectories: ['node_modules'], 14 | moduleNameMapper: { 15 | '@app/(.*)': '/src/$1', 16 | '@dex/(.*)': '/src/dex/$1', 17 | '@providers/(.*)': '/src/providers/$1', 18 | '@requests/(.*)': '/src/requests/$1', 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@indigo-labs/dexter", 3 | "version": "5.4.9", 4 | "license": "MIT", 5 | "author": "Zachary Sluder", 6 | "keywords": [ 7 | "dexter", 8 | "Cardano", 9 | "DEX" 10 | ], 11 | "description": "Customizable Typescript SDK for interacting with Cardano DEXs", 12 | "type": "module", 13 | "main": "./build/index.js", 14 | "types": "./build/index.d.ts", 15 | "files": [ 16 | "build/**/*" 17 | ], 18 | "scripts": { 19 | "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json", 20 | "test": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js", 21 | "prepare": "npm run build", 22 | "prepublishOnly": "npm run test" 23 | }, 24 | "dependencies": { 25 | "@types/blake2b": "^2.1.3", 26 | "@types/crypto-js": "^4.1.1", 27 | "axios": "^0.26.1", 28 | "axios-retry": "^3.5.1", 29 | "blake2b": "^2.1.4", 30 | "bottleneck": "^2.19.5", 31 | "crypto-js": "^4.1.1", 32 | "int64-buffer": "^1.0.1", 33 | "js-encoding-utils": "^0.7.3", 34 | "lodash": "^4.17.21", 35 | "lucid-cardano": "^0.10.9" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.21.4", 39 | "@babel/preset-env": "^7.21.4", 40 | "@babel/preset-typescript": "^7.21.4", 41 | "@types/jest": "^29.5.0", 42 | "@types/lodash": "^4.14.202", 43 | "babel-jest": "^29.5.0", 44 | "jest": "^29.5.0", 45 | "prettier": "^2.8.8", 46 | "ts-jest": "^29.1.0", 47 | "ts-node": "^10.9.1", 48 | "tsc-alias": "^1.8.6", 49 | "typescript": "^5.0.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export enum MetadataKey { 2 | Message = 674, 3 | } 4 | 5 | export enum DatumParameterKey { 6 | /** 7 | * Generics. 8 | */ 9 | Action = 'Action', 10 | TokenPolicyId = 'TokenPolicyId', 11 | TokenAssetName = 'TokenAssetName', 12 | ReserveA = 'ReserveA', 13 | ReserveB = 'ReserveB', 14 | CancelDatum = 'CancelDatum', 15 | AScale = 'AScale', 16 | BScale = 'BScale', 17 | 18 | /** 19 | * Swap/wallet info. 20 | */ 21 | Address = 'Address', 22 | SenderPubKeyHash = 'SenderPubKeyHash', 23 | SenderStakingKeyHash = 'SenderStakingKeyHash', 24 | SenderKeyHashes = 'SenderKeyHashes', 25 | ReceiverPubKeyHash = 'ReceiverPubKeyHash', 26 | ReceiverStakingKeyHash = 'ReceiverStakingKeyHash', 27 | SwapInAmount = 'SwapInAmount', 28 | SwapInTokenPolicyId = 'SwapInTokenPolicyId', 29 | SwapInTokenAssetName = 'SwapInTokenAssetName', 30 | SwapOutTokenPolicyId = 'SwapOutTokenPolicyId', 31 | SwapOutTokenAssetName = 'SwapOutTokenAssetName', 32 | MinReceive = 'MinReceive', 33 | Expiration = 'Expiration', 34 | AllowPartialFill = 'AllowPartialFill', 35 | Direction = 'Direction', 36 | FeePaymentKeyHash = 'FeePaymentKeyHash', 37 | Beacon = 'Beacon', 38 | Batcher = 'Batcher', 39 | InToken = 'InToken', 40 | 41 | /** 42 | * Trading fees. 43 | */ 44 | TotalFees = 'TotalFees', 45 | BatcherFee = 'BatcherFee', 46 | DepositFee = 'DepositFee', 47 | ScooperFee = 'ScooperFee', 48 | BaseFee = 'BaseFee', 49 | ExecutionFee = 'ExecutionFee', 50 | FeeSharingNumerator = 'FeeSharingNumerator', 51 | OpeningFee = 'OpeningFee', 52 | FinalFee = 'FinalFee', 53 | FeesFinalized = 'FeesFinalized', 54 | MarketOpen = 'MarketOpen', 55 | ProtocolFee = 'ProtocolFee', 56 | SwapFee = 'SwapFee', 57 | ProjectFeeInBasis = 'ProjectFeeInBasis', 58 | ReserveFeeInBasis = 'ReserveFeeInBasis', 59 | FeeBasis = 'FeeBasis', 60 | AgentFee = 'AgentFee', 61 | 62 | /** 63 | * LP info. 64 | */ 65 | PoolIdentifier = 'PoolIdentifier', 66 | TotalLpTokens = 'TotalLpTokens', 67 | LpTokenPolicyId = 'LpTokenPolicyId', 68 | LpTokenAssetName = 'LpTokenAssetName', 69 | LpFee = 'LpFee', 70 | LpFeeNumerator = 'LpFeeNumerator', 71 | LpFeeDenominator = 'LpFeeDenominator', 72 | PoolAssetAPolicyId = 'PoolAssetAPolicyId', 73 | PoolAssetAAssetName = 'PoolAssetAAssetName', 74 | PoolAssetATreasury = 'PoolAssetATreasury', 75 | PoolAssetABarFee = 'PoolAssetABarFee', 76 | PoolAssetBPolicyId = 'PoolAssetBPolicyId', 77 | PoolAssetBAssetName = 'PoolAssetBAssetName', 78 | PoolAssetBTreasury = 'PoolAssetBTreasury', 79 | PoolAssetBBarFee = 'PoolAssetBBarFee', 80 | RootKLast = 'RootKLast', 81 | LastInteraction = 'LastInteraction', 82 | RequestScriptHash = 'RequestScriptHash', 83 | StakeAdminPolicy = 'StakeAdminPolicy', 84 | LqBound = 'LqBound', 85 | 86 | Unknown = 'Unknown', 87 | } 88 | 89 | export enum TransactionStatus { 90 | Building, 91 | Signing, 92 | Submitting, 93 | Submitted, 94 | Errored, 95 | } 96 | 97 | export enum AddressType { 98 | Contract, 99 | Base, 100 | Enterprise, 101 | } 102 | -------------------------------------------------------------------------------- /src/definition-builder.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameters, DefinitionConstr, DefinitionField } from './types'; 2 | import { DatumParameterKey } from './constants'; 3 | import _ from 'lodash'; 4 | import { datumJsonToCbor } from '@app/utils'; 5 | 6 | export class DefinitionBuilder { 7 | private _definition: DefinitionConstr; 8 | 9 | /** 10 | * Load a DEX definition file as a template for this builder. 11 | */ 12 | public async loadDefinition(definition: DefinitionConstr): Promise { 13 | this._definition = _.cloneDeepWith(definition, (value: any) => { 14 | if (value instanceof Function) { 15 | return value; 16 | } 17 | }); 18 | 19 | return this; 20 | } 21 | 22 | /** 23 | * Push specified parameters to the definition template. 24 | */ 25 | public pushParameters(parameters: DatumParameters): DefinitionBuilder { 26 | if (!this._definition) { 27 | throw new Error(`Definition file must be loaded before applying parameters`); 28 | } 29 | 30 | this._definition = this.applyParameters(this._definition, parameters); 31 | 32 | return this; 33 | } 34 | 35 | /** 36 | * Pull parameters of a datum using a definition template. 37 | */ 38 | public pullParameters(definedDefinition: DefinitionConstr): DatumParameters { 39 | if (!this._definition) { 40 | throw new Error(`Definition file must be loaded before pulling parameters`); 41 | } 42 | 43 | return this.extractParameters(definedDefinition, this._definition); 44 | } 45 | 46 | /** 47 | * Retrieve the CBOR for the builder. 48 | */ 49 | public getCbor(): string { 50 | return datumJsonToCbor(JSON.parse(JSON.stringify(this._definition))); 51 | } 52 | 53 | /** 54 | * Recursively set specified parameters. 55 | */ 56 | private applyParameters(field: DefinitionField, mappedParameters: DatumParameters): DefinitionConstr { 57 | if (field instanceof Function) { 58 | return field(field, mappedParameters, false); 59 | } 60 | 61 | if ('fields' in field) { 62 | if (typeof field.constructor === 'string') { 63 | const parameterValue: any = mappedParameters[field.constructor as keyof typeof DatumParameterKey]; 64 | 65 | if (typeof parameterValue !== 'number') { 66 | throw new Error(`Invalid parameter value '${parameterValue}' for constructor value`); 67 | } 68 | 69 | field.constructor = parameterValue; 70 | } 71 | 72 | field.fields = field.fields.map((fieldParameter: DefinitionField) => { 73 | return this.applyParameters(fieldParameter, mappedParameters); 74 | }); 75 | } 76 | 77 | if ('list' in field) { 78 | field.list = (field.list as DefinitionField[])?.map((fieldParameter: DefinitionField) => { 79 | return this.applyParameters(fieldParameter, mappedParameters); 80 | }); 81 | } 82 | 83 | if ('int' in field) { 84 | let parameterValue: any = mappedParameters[field.int as keyof typeof DatumParameterKey]; 85 | 86 | if (typeof parameterValue === 'bigint') { 87 | parameterValue = Number(parameterValue); 88 | } 89 | if (typeof parameterValue !== 'number') { 90 | throw new Error(`Invalid parameter value '${parameterValue}' for type 'int'`); 91 | } 92 | 93 | field.int = parameterValue; 94 | } 95 | 96 | if ('bytes' in field) { 97 | const parameterValue: any = mappedParameters[field.bytes as keyof typeof DatumParameterKey] ?? ''; 98 | 99 | if (typeof parameterValue !== 'string') { 100 | throw new Error(`Invalid parameter value '${parameterValue}' for type 'bytes'`); 101 | } 102 | 103 | field.bytes = parameterValue; 104 | } 105 | 106 | if (Array.isArray(field) && field.every((item) => typeof item === 'object' && Object.keys(item).length === 1)) { 107 | field.forEach((value) => { 108 | return this.applyParameters(value, mappedParameters); 109 | }); 110 | } 111 | 112 | return field as DefinitionConstr; 113 | } 114 | 115 | /** 116 | * Recursively pull parameters from datum using definition template. 117 | */ 118 | private extractParameters(definedDefinition: DefinitionField, templateDefinition: DefinitionField, foundParameters: DatumParameters = {}): DatumParameters { 119 | if (! templateDefinition) return foundParameters; 120 | 121 | if (templateDefinition instanceof Function) { 122 | templateDefinition(definedDefinition, foundParameters); 123 | 124 | return foundParameters; 125 | } 126 | 127 | if (templateDefinition instanceof Array) { 128 | templateDefinition 129 | .map((fieldParameter: DefinitionField, index: number) => { 130 | return this.extractParameters(fieldParameter, templateDefinition[index], foundParameters); 131 | }) 132 | .forEach((parameters: DatumParameters) => { 133 | foundParameters = { ...foundParameters, ...parameters }; 134 | }); 135 | } 136 | 137 | if ('fields' in definedDefinition) { 138 | if (!('fields' in templateDefinition)) { 139 | throw new Error("Template definition does not match with 'fields'"); 140 | } 141 | 142 | if (templateDefinition.constructor && typeof templateDefinition.constructor !== 'number') { 143 | foundParameters[templateDefinition.constructor] = definedDefinition.constructor; 144 | } else if (templateDefinition.constructor !== definedDefinition.constructor) { 145 | throw new Error('Template definition does not match with constructor value'); 146 | } 147 | 148 | definedDefinition.fields 149 | .map((fieldParameter: DefinitionField, index: number) => { 150 | return this.extractParameters(fieldParameter, templateDefinition.fields[index], foundParameters); 151 | }) 152 | .forEach((parameters: DatumParameters) => { 153 | foundParameters = { ...foundParameters, ...parameters }; 154 | }); 155 | } 156 | 157 | if ('list' in definedDefinition) { 158 | if (!('list' in templateDefinition) || !Array.isArray(definedDefinition.list)) { 159 | throw new Error("Template definition does not match or 'list' is not an array"); 160 | } 161 | 162 | (definedDefinition.list as DefinitionField[]) 163 | .map((fieldParameter: DefinitionField, index: number) => { 164 | // Ensure the template list at index exists before attempting to access it 165 | if (templateDefinition.list as DefinitionField[]) { 166 | return this.extractParameters(fieldParameter, (templateDefinition.list as DefinitionField[])[index], foundParameters); 167 | } else { 168 | throw new Error(`Template definition at index ${index} is undefined`); 169 | } 170 | }) 171 | .forEach((parameters: any) => { 172 | foundParameters = { ...foundParameters, ...parameters }; 173 | }); 174 | } 175 | 176 | if ('int' in definedDefinition) { 177 | if (!('int' in templateDefinition)) { 178 | throw new Error("Template definition does not match with 'int'"); 179 | } 180 | 181 | if (typeof templateDefinition.int !== 'number') { 182 | foundParameters[templateDefinition.int] = definedDefinition.int; 183 | } 184 | } 185 | 186 | if ('bytes' in definedDefinition) { 187 | if (!('bytes' in templateDefinition)) { 188 | throw new Error("Template definition does not match with 'bytes'"); 189 | } 190 | 191 | const datumKeys: string[] = Object.values(DatumParameterKey); 192 | 193 | if (datumKeys.includes(templateDefinition.bytes)) { 194 | foundParameters[templateDefinition.bytes] = definedDefinition.bytes; 195 | } 196 | } 197 | 198 | return foundParameters; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/dex/api/base-api.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../models/asset'; 2 | import { LiquidityPool } from '../models/liquidity-pool'; 3 | import { BaseDex } from '../base-dex'; 4 | import { AxiosInstance } from 'axios'; 5 | 6 | export abstract class BaseApi { 7 | 8 | protected abstract readonly api: AxiosInstance; 9 | protected abstract readonly dex: BaseDex; 10 | 11 | /** 12 | * Fetch all liquidity pools matching assetA & assetB. 13 | */ 14 | abstract liquidityPools(assetA?: Token, assetB?: Token): Promise; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/dex/api/minswap-api.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from './base-api'; 2 | import { Asset, Token } from '../models/asset'; 3 | import { LiquidityPool } from '../models/liquidity-pool'; 4 | import axios, { AxiosInstance } from 'axios'; 5 | import { Minswap } from '../minswap'; 6 | import { RequestConfig } from '@app/types'; 7 | import AES from 'crypto-js/aes'; 8 | import Utf8 from 'crypto-js/enc-utf8'; 9 | import { appendSlash } from '@app/utils'; 10 | 11 | const AES_KEY: string = '22eaca439bfd89cf125827a7a33fe3970d735dbfd5d84f19dd95820781fc47be'; 12 | 13 | export class MinswapApi extends BaseApi { 14 | 15 | protected readonly api: AxiosInstance; 16 | protected readonly dex: Minswap; 17 | 18 | constructor(dex: Minswap, requestConfig: RequestConfig) { 19 | super(); 20 | 21 | this.dex = dex; 22 | 23 | this.api = axios.create({ 24 | timeout: requestConfig.timeout, 25 | baseURL: `${appendSlash(requestConfig.proxyUrl)}https://monorepo-mainnet-prod.minswap.org/graphql`, 26 | withCredentials: false, 27 | }); 28 | } 29 | 30 | liquidityPools(assetA: Token, assetB?: Token): Promise { 31 | // Small optimization for providing both tokens 32 | if (assetA && assetB) { 33 | return this.poolsByPair(assetA, assetB) 34 | .then((pool: LiquidityPool) => [pool]); 35 | } 36 | 37 | const maxPerPage: number = 20; 38 | 39 | const getPaginatedResponse = (page: number): Promise => { 40 | return this.api.post('', { 41 | operationName: 'PoolsByAsset', 42 | query: ` 43 | query PoolsByAsset($asset: InputAsset!, $limit: Int, $offset: Int) { 44 | poolsByAsset( 45 | asset: $asset 46 | limit: $limit 47 | offset: $offset 48 | ) { 49 | assetA { 50 | currencySymbol 51 | tokenName 52 | ...allMetadata 53 | } 54 | assetB { 55 | currencySymbol 56 | tokenName 57 | ...allMetadata 58 | } 59 | reserveA 60 | reserveB 61 | lpAsset { 62 | currencySymbol 63 | tokenName 64 | } 65 | totalLiquidity 66 | } 67 | } 68 | fragment allMetadata on Asset { 69 | metadata { 70 | name 71 | decimals 72 | } 73 | } 74 | `, 75 | variables: { 76 | asset: { 77 | currencySymbol: assetA === 'lovelace' ? '' : assetA.policyId, 78 | tokenName: assetA === 'lovelace' ? '' : assetA.nameHex, 79 | }, 80 | limit: maxPerPage, 81 | offset: page * maxPerPage, 82 | }, 83 | }).then((response: any) => { 84 | response = JSON.parse(this.decryptResponse(response.data.data.encryptedData)); 85 | 86 | const pools = response.poolsByAsset; 87 | 88 | const liquidityPools = pools.map((pool: any) => this.liquidityPoolFromResponse(pool)); 89 | 90 | if (pools.length < maxPerPage) { 91 | return liquidityPools; 92 | } 93 | 94 | return getPaginatedResponse(page + 1).then((nextPagePools: LiquidityPool[]) => { 95 | return liquidityPools.concat(nextPagePools); 96 | }); 97 | }); 98 | }; 99 | 100 | return getPaginatedResponse(0); 101 | } 102 | 103 | private poolsByPair(assetA: Token, assetB: Token): Promise { 104 | return this.api.post('', { 105 | operationName: 'PoolByPair', 106 | query: ` 107 | query PoolByPair($pair: InputPoolByPair!) { 108 | poolByPair(pair: $pair) { 109 | assetA { 110 | currencySymbol 111 | tokenName 112 | isVerified 113 | ...allMetadata 114 | } 115 | assetB { 116 | currencySymbol 117 | tokenName 118 | isVerified 119 | ...allMetadata 120 | } 121 | reserveA 122 | reserveB 123 | lpAsset { 124 | currencySymbol 125 | tokenName 126 | } 127 | totalLiquidity 128 | profitSharing { 129 | feeTo 130 | } 131 | } 132 | } 133 | fragment allMetadata on Asset { 134 | metadata { 135 | name 136 | ticker 137 | url 138 | decimals 139 | description 140 | } 141 | } 142 | `, 143 | variables: { 144 | pair: { 145 | assetA: { 146 | currencySymbol: assetA === 'lovelace' ? '' : assetA.policyId, 147 | tokenName: assetA === 'lovelace' ? '' : assetA.nameHex, 148 | }, 149 | assetB: { 150 | currencySymbol: assetB === 'lovelace' ? '' : assetB.policyId, 151 | tokenName: assetB === 'lovelace' ? '' : assetB.nameHex, 152 | }, 153 | }, 154 | }, 155 | }).then((response: any) => { 156 | response = JSON.parse(this.decryptResponse(response.data.data.encryptedData)); 157 | 158 | return this.liquidityPoolFromResponse(response.poolByPair) 159 | }); 160 | } 161 | 162 | private liquidityPoolFromResponse(poolData: any): LiquidityPool { 163 | const liquidityPool: LiquidityPool = new LiquidityPool( 164 | Minswap.identifier, 165 | poolData.assetA.currencySymbol !== '' 166 | ? new Asset(poolData.assetA.currencySymbol, poolData.assetA.tokenName, poolData.assetA.metadata?.decimals ?? 0) 167 | : 'lovelace', 168 | poolData.assetB.currencySymbol !== '' 169 | ? new Asset(poolData.assetB.currencySymbol, poolData.assetB.tokenName, poolData.assetB.metadata?.decimals ?? 0) 170 | : 'lovelace', 171 | BigInt(poolData.reserveA), 172 | BigInt(poolData.reserveB), 173 | '', // Not provided 174 | this.dex.marketOrderAddress, 175 | this.dex.limitOrderAddress, 176 | ); 177 | 178 | liquidityPool.lpToken = new Asset(poolData.lpAsset.currencySymbol, poolData.lpAsset.tokenName); 179 | liquidityPool.totalLpTokens = BigInt(poolData.totalLiquidity); 180 | liquidityPool.poolFeePercent = 0.3; 181 | liquidityPool.identifier = liquidityPool.lpToken.identifier(); 182 | 183 | return liquidityPool; 184 | } 185 | 186 | private decryptResponse(encryptedResponse: string): any { 187 | return AES.decrypt(encryptedResponse, AES_KEY).toString(Utf8); 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/dex/api/muesliswap-api.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from './base-api'; 2 | import { Asset, Token } from '../models/asset'; 3 | import { LiquidityPool } from '../models/liquidity-pool'; 4 | import axios, { AxiosInstance } from 'axios'; 5 | import { MuesliSwap } from '../muesliswap'; 6 | import { RequestConfig } from '@app/types'; 7 | import { appendSlash } from '@app/utils'; 8 | 9 | export class MuesliSwapApi extends BaseApi { 10 | 11 | protected readonly api: AxiosInstance; 12 | protected readonly dex: MuesliSwap; 13 | 14 | constructor(dex: MuesliSwap, requestConfig: RequestConfig) { 15 | super(); 16 | 17 | this.dex = dex; 18 | this.api = axios.create({ 19 | timeout: requestConfig.timeout, 20 | baseURL: `${appendSlash(requestConfig.proxyUrl)}https://api.muesliswap.com/`, 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | } 24 | }); 25 | } 26 | 27 | liquidityPools(assetA: Token, assetB?: Token): Promise { 28 | const providers: string[] = ['muesliswap', 'muesliswap_v2', 'muesliswap_clp']; 29 | const tokenA: string = (assetA === 'lovelace') 30 | ? '.' 31 | : assetA.identifier('.'); 32 | const tokenB: string = (assetB && assetB !== 'lovelace') 33 | ? assetB.identifier('.') 34 | : ''; 35 | 36 | return this.api.get(`/liquidity/pools?providers=${providers.join(',')}&token-a=${tokenA}&token-b=${tokenB}`) 37 | .then((response: any) => { 38 | return response.data.map((pool: any) => { 39 | let liquidityPool: LiquidityPool = new LiquidityPool( 40 | MuesliSwap.identifier, 41 | pool.tokenA.symbol !== 'ADA' 42 | ? new Asset(pool.tokenA.address.policyId, pool.tokenA.address.name, pool.tokenA.decimalPlaces) 43 | : 'lovelace', 44 | pool.tokenB.symbol !== 'ADA' 45 | ? new Asset(pool.tokenB.address.policyId, pool.tokenB.address.name, pool.tokenB.decimalPlaces) 46 | : 'lovelace', 47 | BigInt(pool.tokenA.amount), 48 | BigInt(pool.tokenB.amount), 49 | pool.batcherAddress, 50 | this.dex.orderAddress, 51 | this.dex.orderAddress, 52 | ); 53 | 54 | liquidityPool.identifier = pool.poolId; 55 | liquidityPool.lpToken = new Asset(pool.lpToken.address.policyId, pool.lpToken.address.name); 56 | liquidityPool.poolFeePercent = Number(pool.poolFee); 57 | liquidityPool.totalLpTokens = BigInt(pool.lpToken.amount); 58 | 59 | return liquidityPool; 60 | }); 61 | }); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/dex/api/splash-api.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from './base-api'; 2 | import { Asset, Token } from '../models/asset'; 3 | import { LiquidityPool } from '../models/liquidity-pool'; 4 | import axios, { AxiosInstance } from 'axios'; 5 | import { RequestConfig } from '@app/types'; 6 | import { appendSlash } from '@app/utils'; 7 | import { Splash } from '@dex/splash'; 8 | 9 | const MAX_INT: bigint = 9_223_372_036_854_775_807n; 10 | 11 | export class SplashApi extends BaseApi { 12 | 13 | protected readonly api: AxiosInstance; 14 | protected readonly dex: Splash; 15 | 16 | constructor(dex: Splash, requestConfig: RequestConfig) { 17 | super(); 18 | 19 | this.dex = dex; 20 | 21 | this.api = axios.create({ 22 | timeout: requestConfig.timeout, 23 | baseURL: `${appendSlash(requestConfig.proxyUrl)}https://api5.splash.trade/platform-api/v1/`, 24 | withCredentials: false, 25 | }); 26 | } 27 | 28 | async liquidityPools(assetA: Token, assetB?: Token): Promise { 29 | const assets: any = (await this.assets()).data['tokens']; 30 | 31 | return this.api.get('/pools/overview?verified=false&duplicated=false').then((response: any) => { 32 | return response.data.map((pool: any) => this.liquidityPoolFromResponse(pool, assets)) as LiquidityPool[]; 33 | }); 34 | } 35 | 36 | private liquidityPoolFromResponse(poolData: any, assets: any): LiquidityPool { 37 | poolData = poolData.pool; 38 | 39 | const tokenA: Token = poolData.x.asset === '.' 40 | ? 'lovelace' 41 | : new Asset(poolData.x.asset.split('.')[0], poolData.x.asset.split('.')[1]); 42 | const tokenB = poolData.y.asset === '.' 43 | ? 'lovelace' 44 | : new Asset(poolData.y.asset.split('.')[0], poolData.y.asset.split('.')[1]); 45 | 46 | if (tokenA !== 'lovelace' && tokenA.identifier('.') in assets) { 47 | tokenA.decimals = assets[tokenA.identifier('.')].decimals; 48 | } 49 | if (tokenB !== 'lovelace' && tokenB.identifier('.') in assets) { 50 | tokenB.decimals = assets[tokenB.identifier('.')].decimals; 51 | } 52 | 53 | const liquidityPool: LiquidityPool = new LiquidityPool( 54 | Splash.identifier, 55 | tokenA, 56 | tokenB, 57 | BigInt(poolData['x']['amount']) - BigInt(poolData['treasuryX']), 58 | BigInt(poolData['y']['amount']) - BigInt(poolData['treasuryY']), 59 | '', 60 | '', 61 | '', 62 | ); 63 | 64 | const [lpTokenPolicyId, lpTokenAssetName] = poolData['lq']['asset'].split('.'); 65 | 66 | liquidityPool.lpToken = new Asset(lpTokenPolicyId, lpTokenAssetName); 67 | liquidityPool.totalLpTokens = MAX_INT - BigInt(poolData['lq']['amount']); 68 | liquidityPool.identifier = poolData['id']; 69 | 70 | return liquidityPool; 71 | } 72 | 73 | private assets(): Promise { 74 | return axios.get('https://spectrum.fi/cardano-token-list-v2.json'); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/dex/api/sundaeswap-v1-api.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from './base-api'; 2 | import { Asset, Token } from '../models/asset'; 3 | import { LiquidityPool } from '../models/liquidity-pool'; 4 | import axios, { AxiosInstance } from 'axios'; 5 | import { SundaeSwapV1 } from '../sundaeswap-v1'; 6 | import { RequestConfig } from '@app/types'; 7 | import { appendSlash } from '@app/utils'; 8 | 9 | export class SundaeSwapV1Api extends BaseApi { 10 | 11 | protected readonly api: AxiosInstance; 12 | protected readonly dex: SundaeSwapV1; 13 | 14 | constructor(dex: SundaeSwapV1, requestConfig: RequestConfig) { 15 | super(); 16 | 17 | this.dex = dex; 18 | this.api = axios.create({ 19 | timeout: requestConfig.timeout, 20 | baseURL: `${appendSlash(requestConfig.proxyUrl)}https://stats.sundaeswap.finance/graphql`, 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | } 24 | }); 25 | } 26 | 27 | liquidityPools(assetA: Token, assetB?: Token): Promise { 28 | const maxPerPage: number = 100; 29 | 30 | const assetAId: string = (assetA === 'lovelace') 31 | ? '' 32 | : assetA.identifier('.'); 33 | let assetBId: string = (assetB && assetB !== 'lovelace') 34 | ? assetB.identifier('.') 35 | : ''; 36 | 37 | const getPaginatedResponse = (page: number): Promise => { 38 | return this.api.post('', { 39 | operationName: 'getPoolsByAssetIds', 40 | query: ` 41 | query getPoolsByAssetIds($assetIds: [String!]!, $pageSize: Int, $page: Int) { 42 | pools(assetIds: $assetIds, pageSize: $pageSize, page: $page) { 43 | ...PoolFragment 44 | } 45 | } 46 | fragment PoolFragment on Pool { 47 | assetA { 48 | ...AssetFragment 49 | } 50 | assetB { 51 | ...AssetFragment 52 | } 53 | assetLP { 54 | ...AssetFragment 55 | } 56 | name 57 | fee 58 | quantityA 59 | quantityB 60 | quantityLP 61 | ident 62 | assetID 63 | } 64 | fragment AssetFragment on Asset { 65 | assetId 66 | decimals 67 | } 68 | `, 69 | variables: { 70 | page: page, 71 | pageSize: maxPerPage, 72 | assetIds: [assetBId !== '' ? assetBId : assetAId], 73 | }, 74 | }).then((response: any) => { 75 | const pools = response.data.data.pools; 76 | const liquidityPools = pools.map((pool: any) => { 77 | let liquidityPool: LiquidityPool = new LiquidityPool( 78 | SundaeSwapV1.identifier, 79 | pool.assetA.assetId 80 | ? Asset.fromIdentifier(pool.assetA.assetId, pool.assetA.decimals) 81 | : 'lovelace', 82 | pool.assetB.assetId 83 | ? Asset.fromIdentifier(pool.assetB.assetId, pool.assetB.decimals) 84 | : 'lovelace', 85 | BigInt(pool.quantityA), 86 | BigInt(pool.quantityB), 87 | this.dex.poolAddress, 88 | this.dex.orderAddress, 89 | this.dex.orderAddress, 90 | ); 91 | 92 | liquidityPool.identifier = pool.ident; 93 | liquidityPool.lpToken = Asset.fromIdentifier(pool.assetLP.assetId); 94 | liquidityPool.poolFeePercent = Number(pool.fee); 95 | liquidityPool.totalLpTokens = BigInt(pool.quantityLP); 96 | 97 | return liquidityPool; 98 | }); 99 | 100 | if (pools.length < maxPerPage) { 101 | return liquidityPools; 102 | } 103 | 104 | return getPaginatedResponse(page + 1).then((nextPagePools: LiquidityPool[]) => { 105 | return liquidityPools.concat(nextPagePools); 106 | }); 107 | }); 108 | }; 109 | 110 | return getPaginatedResponse(0); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/dex/api/sundaeswap-v3-api.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from './base-api'; 2 | import { Asset, Token } from '../models/asset'; 3 | import { LiquidityPool } from '../models/liquidity-pool'; 4 | import axios, { AxiosInstance } from 'axios'; 5 | import { RequestConfig } from '@app/types'; 6 | import { appendSlash } from '@app/utils'; 7 | import { SundaeSwapV3 } from '@dex/sundaeswap-v3'; 8 | 9 | export class SundaeSwapV3Api extends BaseApi { 10 | 11 | protected readonly api: AxiosInstance; 12 | protected readonly dex: SundaeSwapV3; 13 | 14 | constructor(dex: SundaeSwapV3, requestConfig: RequestConfig) { 15 | super(); 16 | 17 | this.dex = dex; 18 | this.api = axios.create({ 19 | timeout: requestConfig.timeout, 20 | baseURL: `${appendSlash(requestConfig.proxyUrl)}https://api.sundae.fi/graphql`, 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | } 24 | }); 25 | } 26 | 27 | async liquidityPools(assetA: Token, assetB?: Token): Promise { 28 | const assetAId: string = (assetA === 'lovelace') 29 | ? 'ada.lovelace' 30 | : assetA.identifier('.'); 31 | const assetBId: string = (assetB && assetB !== 'lovelace') 32 | ? assetB.identifier('.') 33 | : 'ada.lovelace'; 34 | const assets: string[] = [assetAId, assetBId]; 35 | 36 | return await this.api.post('', { 37 | operationName: 'fetchPoolsByPair', 38 | query: `query fetchPoolsByPair($assetA: ID!, $assetB: ID!) {\n pools {\n byPair(assetA: $assetA, assetB: $assetB) {\n ...PoolBrambleFragment\n }\n }\n}\n\nfragment PoolBrambleFragment on Pool {\n id\n assetA {\n ...AssetBrambleFragment\n }\n assetB {\n ...AssetBrambleFragment\n }\n assetLP {\n ...AssetBrambleFragment\n }\n feesFinalized {\n slot\n }\n marketOpen {\n slot\n }\n askFee\n bidFee\n feeManagerId\n current {\n quantityA {\n quantity\n }\n quantityB {\n quantity\n }\n quantityLP {\n quantity\n }\n tvl {\n quantity\n }\n }\n version\n}\n\nfragment AssetBrambleFragment on Asset {\n id\n policyId\n description\n dateListed {\n format\n }\n decimals\n ticker\n name\n logo\n assetName\n metadata {\n ... on OnChainLabel20 {\n __typename\n }\n ... on OnChainLabel721 {\n __typename\n }\n ... on CardanoTokenRegistry {\n __typename\n }\n }\n}`, 39 | variables: { 40 | assetA: assets[0], 41 | assetB: assets[1], 42 | }, 43 | }).then((response: any) => { 44 | const pools = response.data.data.pools.byPair; 45 | 46 | return pools 47 | .filter((pool: any) => pool.version === 'V3') 48 | .map((pool: any) => { 49 | let liquidityPool: LiquidityPool = new LiquidityPool( 50 | SundaeSwapV3.identifier, 51 | pool.assetA.id === 'ada.lovelace' 52 | ? 'lovelace' 53 | : Asset.fromIdentifier(pool.assetA.id, pool.assetA.decimals), 54 | pool.assetB.id === 'ada.lovelace' 55 | ? 'lovelace' 56 | : Asset.fromIdentifier(pool.assetB.id, pool.assetB.decimals), 57 | BigInt(pool.current.quantityA.quantity), 58 | BigInt(pool.current.quantityB.quantity), 59 | this.dex.poolAddress, 60 | '', 61 | '', 62 | ); 63 | 64 | liquidityPool.identifier = pool.id; 65 | liquidityPool.lpToken = Asset.fromIdentifier(pool.assetLP.id); 66 | liquidityPool.poolFeePercent = Number((pool.bidFee[0] / pool.bidFee[1]) * 100); 67 | liquidityPool.totalLpTokens = BigInt(pool.current.quantityLP.quantity); 68 | 69 | return liquidityPool; 70 | }); 71 | }); 72 | 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/dex/api/vyfinance-api.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from './base-api'; 2 | import { Asset, Token } from '../models/asset'; 3 | import { LiquidityPool } from '../models/liquidity-pool'; 4 | import axios, { AxiosInstance } from 'axios'; 5 | import { VyFinance } from '../vyfinance'; 6 | import { RequestConfig } from '@app/types'; 7 | import { appendSlash } from '@app/utils'; 8 | 9 | export class VyfinanceApi extends BaseApi { 10 | 11 | protected readonly api: AxiosInstance; 12 | protected readonly dex: VyFinance; 13 | 14 | constructor(dex: VyFinance, requestConfig: RequestConfig) { 15 | super(); 16 | 17 | this.dex = dex; 18 | this.api = axios.create({ 19 | timeout: requestConfig.timeout, 20 | baseURL: `${appendSlash(requestConfig.proxyUrl)}https://api.vyfi.io`, 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | } 24 | }); 25 | } 26 | 27 | liquidityPools(assetA?: Token, assetB?: Token): Promise { 28 | const assetAId: string = (assetA && assetA !== 'lovelace') 29 | ? assetA.identifier() 30 | : 'lovelace'; 31 | let assetBId: string = (assetB && assetB !== 'lovelace') 32 | ? assetB.identifier() 33 | : 'lovelace'; 34 | 35 | const url: string = assetA && assetB 36 | ? `/lp?networkId=1&v2=true&tokenAUnit=${assetAId}&tokenBUnit=${assetBId}` 37 | : '/lp?networkId=1&v2=true'; 38 | 39 | return this.api.get(url) 40 | .then((poolResponse: any) => { 41 | return poolResponse.data.map((pool: any) => { 42 | const poolDetails: any = JSON.parse(pool.json); 43 | 44 | const tokenA: Token = poolDetails['aAsset']['tokenName'] 45 | ? new Asset(poolDetails['aAsset']['currencySymbol'], Buffer.from(poolDetails['aAsset']['tokenName']).toString('hex')) 46 | : 'lovelace'; 47 | const tokenB: Token = poolDetails['bAsset']['tokenName'] 48 | ? new Asset(poolDetails['bAsset']['currencySymbol'], Buffer.from(poolDetails['bAsset']['tokenName']).toString('hex')) 49 | : 'lovelace'; 50 | 51 | 52 | let liquidityPool: LiquidityPool = new LiquidityPool( 53 | VyFinance.identifier, 54 | tokenA, 55 | tokenB, 56 | BigInt(pool['tokenAQuantity'] ?? 0), 57 | BigInt(pool['tokenBQuantity'] ?? 0), 58 | pool['poolValidatorUtxoAddress'], 59 | pool['orderValidatorUtxoAddress'], 60 | pool['orderValidatorUtxoAddress'], 61 | ); 62 | 63 | const lpTokenDetails: string[] = pool['lpPolicyId-assetId'].split('-'); 64 | liquidityPool.lpToken = new Asset(lpTokenDetails[0], lpTokenDetails[1]); 65 | liquidityPool.poolFeePercent = (poolDetails['feesSettings']['barFee'] + poolDetails['feesSettings']['liqFee']) / 100; 66 | liquidityPool.identifier = liquidityPool.lpToken.identifier(); 67 | liquidityPool.extra.nft = new Asset(poolDetails['mainNFT']['currencySymbol'], poolDetails['mainNFT']['tokenName']); 68 | 69 | return liquidityPool; 70 | }).filter((pool: LiquidityPool | undefined) => pool !== undefined) as LiquidityPool[]; 71 | }).catch((e) => { 72 | console.error(e) 73 | return []; 74 | }); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/dex/api/wingriders-api.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from './base-api'; 2 | import { Asset, Token } from '../models/asset'; 3 | import { LiquidityPool } from '../models/liquidity-pool'; 4 | import axios, { AxiosInstance } from 'axios'; 5 | import { RequestConfig } from '@app/types'; 6 | import { WingRiders } from '@dex/wingriders'; 7 | import { appendSlash } from '@app/utils'; 8 | 9 | export class WingRidersApi extends BaseApi { 10 | 11 | protected readonly api: AxiosInstance; 12 | protected readonly dex: WingRiders; 13 | 14 | constructor(dex: WingRiders, requestConfig: RequestConfig) { 15 | super(); 16 | 17 | this.dex = dex; 18 | this.api = axios.create({ 19 | timeout: requestConfig.timeout, 20 | baseURL: `${appendSlash(requestConfig.proxyUrl)}https://api.mainnet.wingriders.com/graphql`, 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | } 24 | }); 25 | } 26 | 27 | liquidityPools(assetA: Token, assetB?: Token): Promise { 28 | return this.api.post('', { 29 | operationName: 'LiquidityPoolsWithMarketData', 30 | query: ` 31 | query LiquidityPoolsWithMarketData($input: PoolsWithMarketdataInput) { 32 | poolsWithMarketdata(input: $input) { 33 | ...LiquidityPoolFragment 34 | } 35 | } 36 | fragment LiquidityPoolFragment on PoolWithMarketdata { 37 | issuedShareToken { 38 | policyId 39 | assetName 40 | quantity 41 | } 42 | tokenA { 43 | policyId 44 | assetName 45 | quantity 46 | } 47 | tokenB { 48 | policyId 49 | assetName 50 | quantity 51 | } 52 | treasuryA 53 | treasuryB 54 | _utxo { 55 | address 56 | } 57 | } 58 | `, 59 | variables: { 60 | input: { 61 | sort: true 62 | }, 63 | }, 64 | }).then((response: any) => { 65 | return response.data.data.poolsWithMarketdata.map((pool: any) => { 66 | const tokenA: Token = pool.tokenA.policyId !== '' 67 | ? new Asset(pool.tokenA.policyId, pool.tokenA.assetName) 68 | : 'lovelace'; 69 | const tokenB: Token = pool.tokenB.policyId !== '' 70 | ? new Asset(pool.tokenB.policyId, pool.tokenB.assetName) 71 | : 'lovelace'; 72 | 73 | let liquidityPool: LiquidityPool = new LiquidityPool( 74 | WingRiders.identifier, 75 | tokenA, 76 | tokenB, 77 | BigInt(pool.tokenA.quantity) - BigInt(pool.treasuryA), 78 | BigInt(pool.tokenB.quantity) - BigInt(pool.treasuryB), 79 | pool._utxo.address, 80 | this.dex.orderAddress, 81 | this.dex.orderAddress, 82 | ); 83 | 84 | liquidityPool.lpToken = new Asset(pool.issuedShareToken.policyId, pool.issuedShareToken.assetName); 85 | liquidityPool.poolFeePercent = 0.35; 86 | liquidityPool.identifier = liquidityPool.lpToken.identifier(); 87 | liquidityPool.totalLpTokens = BigInt(pool.issuedShareToken.quantity); 88 | 89 | return liquidityPool; 90 | }).filter((pool: LiquidityPool | undefined) => pool !== undefined); 91 | }); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/dex/base-dex.ts: -------------------------------------------------------------------------------- 1 | import { LiquidityPool } from './models/liquidity-pool'; 2 | import { BaseDataProvider } from '@providers/data/base-data-provider'; 3 | import { Asset, Token } from './models/asset'; 4 | import { AssetBalance, DatumParameters, PayToAddress, SpendUTxO, SwapFee, UTxO } from '@app/types'; 5 | import { DatumParameterKey } from '@app/constants'; 6 | import { tokensMatch } from '@app/utils'; 7 | import { BaseApi } from '@dex/api/base-api'; 8 | import { BaseWalletProvider } from '@providers/wallet/base-wallet-provider'; 9 | 10 | export abstract class BaseDex { 11 | 12 | /** 13 | * API connection for the DEX. 14 | */ 15 | public abstract readonly api: BaseApi; 16 | 17 | /** 18 | * Fetch addresses mapped to a liquidity pool. 19 | */ 20 | abstract liquidityPoolAddresses(provider?: BaseDataProvider): Promise; 21 | 22 | /** 23 | * Fetch all liquidity pools. 24 | */ 25 | abstract liquidityPools(provider: BaseDataProvider, wallet?: BaseWalletProvider): Promise; 26 | 27 | /** 28 | * Craft liquidity pool state from a valid UTxO. 29 | */ 30 | abstract liquidityPoolFromUtxo(provider: BaseDataProvider, utxo: UTxO, wallet?: BaseWalletProvider): Promise; 31 | 32 | /** 33 | * Estimated swap in amount given for a swap out token & amount on a liquidity pool. 34 | */ 35 | abstract estimatedGive(liquidityPool: LiquidityPool, swapOutToken: Token, swapOutAmount: bigint): bigint; 36 | 37 | /** 38 | * Estimated swap out amount received for a swap in token & amount on a liquidity pool. 39 | */ 40 | abstract estimatedReceive(liquidityPool: LiquidityPool, swapInToken: Token, swapInAmount: bigint): bigint; 41 | 42 | /** 43 | * Calculated price impact after for swap order. 44 | */ 45 | abstract priceImpactPercent(liquidityPool: LiquidityPool, swapInToken: Token, swapInAmount: bigint): number; 46 | 47 | /** 48 | * Craft a swap order for this DEX. 49 | */ 50 | abstract buildSwapOrder(liquidityPool: LiquidityPool, swapParameters: DatumParameters, spendUtxos?: SpendUTxO[], dataProvider?: BaseDataProvider): Promise; 51 | 52 | /** 53 | * Craft a swap order cancellation for this DEX. 54 | */ 55 | abstract buildCancelSwapOrder(txOutputs: UTxO[], returnAddress: string, wallet?: BaseWalletProvider): Promise; 56 | 57 | /** 58 | * Fees associated with submitting a swap order. 59 | */ 60 | abstract swapOrderFees(liquidityPool?: LiquidityPool, swapInToken?: Token, swapInAmount?: bigint): SwapFee[]; 61 | 62 | /** 63 | * Adjust the payment for the DEX order address to include the swap in amount. 64 | */ 65 | protected buildSwapOrderPayment(swapParameters: DatumParameters, orderPayment: PayToAddress): PayToAddress { 66 | const swapInAmount: bigint = swapParameters[DatumParameterKey.SwapInAmount] as bigint; 67 | const swapInToken: Token = swapParameters[DatumParameterKey.SwapInTokenPolicyId] 68 | ? new Asset( 69 | swapParameters[DatumParameterKey.SwapInTokenPolicyId] as string, 70 | swapParameters[DatumParameterKey.SwapInTokenAssetName] as string, 71 | ) 72 | : 'lovelace'; 73 | 74 | let assetBalance: AssetBalance | undefined = orderPayment.assetBalances.find((payment: AssetBalance) => { 75 | return tokensMatch(payment.asset, swapInToken); 76 | }); 77 | 78 | if (! assetBalance) { 79 | orderPayment.assetBalances.push({ 80 | asset: swapInToken, 81 | quantity: swapInAmount, 82 | }); 83 | } else { 84 | assetBalance.quantity += swapInAmount; 85 | } 86 | 87 | return orderPayment; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/dex/definitions/minswap-v2/order.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | /** 4 | * https://github.com/minswap/minswap-dex-v2/blob/main/src/types/order.ts 5 | */ 6 | export default { 7 | constructor: 0, 8 | fields: [ 9 | { 10 | constructor: 0, 11 | fields: [ 12 | { 13 | bytes: DatumParameterKey.SenderPubKeyHash, 14 | } 15 | ] 16 | }, 17 | { 18 | constructor: 0, 19 | fields: [ 20 | { 21 | constructor: 0, 22 | fields: [ 23 | { 24 | bytes: DatumParameterKey.SenderPubKeyHash, 25 | } 26 | ] 27 | }, 28 | { 29 | constructor: 0, 30 | fields: [ 31 | { 32 | constructor: 0, 33 | fields: [ 34 | { 35 | constructor: 0, 36 | fields: [ 37 | { 38 | bytes: DatumParameterKey.SenderStakingKeyHash, 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | ] 47 | }, 48 | { 49 | constructor: 0, 50 | fields: [] 51 | }, 52 | { 53 | constructor: 0, 54 | fields: [ 55 | { 56 | constructor: 0, 57 | fields: [ 58 | { 59 | bytes: DatumParameterKey.SenderPubKeyHash, 60 | } 61 | ] 62 | }, 63 | { 64 | constructor: 0, 65 | fields: [ 66 | { 67 | constructor: 0, 68 | fields: [ 69 | { 70 | constructor: 0, 71 | fields: [ 72 | { 73 | bytes: DatumParameterKey.SenderStakingKeyHash, 74 | } 75 | ] 76 | } 77 | ] 78 | } 79 | ] 80 | } 81 | ] 82 | }, 83 | { 84 | constructor: 0, 85 | fields: [] 86 | }, 87 | { 88 | constructor: 0, 89 | fields: [ 90 | { 91 | bytes: DatumParameterKey.LpTokenPolicyId 92 | }, 93 | { 94 | bytes: DatumParameterKey.LpTokenAssetName 95 | } 96 | ] 97 | }, 98 | { 99 | constructor: 0, 100 | fields: [ 101 | { 102 | constructor: DatumParameterKey.Direction, 103 | fields: [] 104 | }, 105 | { 106 | constructor: 0, 107 | fields: [ 108 | { 109 | int: DatumParameterKey.SwapInAmount 110 | } 111 | ] 112 | }, 113 | { 114 | int: DatumParameterKey.MinReceive 115 | }, 116 | { 117 | constructor: 0, 118 | fields: [] 119 | } 120 | ] 121 | }, 122 | { 123 | int: DatumParameterKey.BatcherFee 124 | }, 125 | { 126 | constructor: 1, 127 | fields: [] 128 | } 129 | ] 130 | } -------------------------------------------------------------------------------- /src/dex/definitions/minswap-v2/pool.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | import { DatumParameters, DefinitionField } from '@app/types'; 3 | 4 | /** 5 | * https://github.com/minswap/minswap-dex-v2/blob/main/src/types/pool.ts 6 | */ 7 | export default { 8 | constructor: 0, 9 | fields: [ 10 | { 11 | constructor: 0, 12 | fields: [ 13 | (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { 14 | return; 15 | }, 16 | ] 17 | }, 18 | { 19 | constructor: 0, 20 | fields: [ 21 | { 22 | bytes: DatumParameterKey.PoolAssetAPolicyId 23 | }, 24 | { 25 | bytes: DatumParameterKey.PoolAssetAAssetName 26 | } 27 | ] 28 | }, 29 | { 30 | constructor: 0, 31 | fields: [ 32 | { 33 | bytes: DatumParameterKey.PoolAssetBPolicyId 34 | }, 35 | { 36 | bytes: DatumParameterKey.PoolAssetBAssetName 37 | } 38 | ] 39 | }, 40 | { 41 | int: DatumParameterKey.TotalLpTokens 42 | }, 43 | { 44 | int: DatumParameterKey.ReserveA 45 | }, 46 | { 47 | int: DatumParameterKey.ReserveB 48 | }, 49 | { 50 | int: DatumParameterKey.BaseFee 51 | }, 52 | { 53 | int: DatumParameterKey.FeeSharingNumerator 54 | }, 55 | (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { 56 | return; 57 | }, 58 | ] 59 | } -------------------------------------------------------------------------------- /src/dex/definitions/minswap/order.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | /** 4 | * https://github.com/CatspersCoffee/contracts/blob/bd6831e6806798032f6bb754d94a06d72d4d28a1/dex/src/Minswap/BatchOrder/Types.hs 5 | */ 6 | export default { 7 | constructor: 0, 8 | fields: [ 9 | { 10 | constructor: 0, 11 | fields: [ 12 | { 13 | constructor: 0, 14 | fields: [ 15 | { 16 | bytes: DatumParameterKey.SenderPubKeyHash, 17 | } 18 | ] 19 | }, 20 | { 21 | constructor: 0, 22 | fields: [ 23 | { 24 | constructor: 0, 25 | fields: [ 26 | { 27 | constructor: 0, 28 | fields: [ 29 | { 30 | bytes: DatumParameterKey.SenderPubKeyHash, 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ] 39 | }, 40 | { 41 | constructor: 0, 42 | fields: [ 43 | { 44 | constructor: 0, 45 | fields: [ 46 | { 47 | bytes: DatumParameterKey.ReceiverPubKeyHash, 48 | } 49 | ] 50 | }, 51 | { 52 | constructor: 0, 53 | fields: [ 54 | { 55 | constructor: 0, 56 | fields: [ 57 | { 58 | constructor: 0, 59 | fields: [ 60 | { 61 | bytes: DatumParameterKey.ReceiverStakingKeyHash, 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | ] 68 | } 69 | ] 70 | }, 71 | { 72 | constructor: 1, 73 | fields: [] 74 | }, 75 | { 76 | constructor: 0, 77 | fields: [ 78 | { 79 | constructor: 0, 80 | fields: [ 81 | { 82 | bytes: DatumParameterKey.SwapOutTokenPolicyId 83 | }, 84 | { 85 | bytes: DatumParameterKey.SwapOutTokenAssetName 86 | } 87 | ] 88 | }, 89 | { 90 | int: DatumParameterKey.MinReceive 91 | } 92 | ] 93 | }, 94 | { 95 | int: DatumParameterKey.BatcherFee 96 | }, 97 | { 98 | int: DatumParameterKey.DepositFee 99 | } 100 | ] 101 | } -------------------------------------------------------------------------------- /src/dex/definitions/minswap/pool.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | /** 4 | * https://github.com/CatspersCoffee/contracts/blob/bd6831e6806798032f6bb754d94a06d72d4d28a1/dex/src/Minswap/ConstantProductPool/OnChain.hs 5 | */ 6 | export default { 7 | constructor: 0, 8 | fields: [ 9 | { 10 | constructor: 0, 11 | fields: [ 12 | { 13 | bytes: DatumParameterKey.PoolAssetAPolicyId 14 | }, 15 | { 16 | bytes: DatumParameterKey.PoolAssetAAssetName 17 | } 18 | ] 19 | }, 20 | { 21 | constructor: 0, 22 | fields: [ 23 | { 24 | bytes: DatumParameterKey.PoolAssetBPolicyId 25 | }, 26 | { 27 | bytes: DatumParameterKey.PoolAssetBAssetName 28 | } 29 | ] 30 | }, 31 | { 32 | int: DatumParameterKey.TotalLpTokens 33 | }, 34 | { 35 | int: DatumParameterKey.RootKLast 36 | }, 37 | { 38 | constructor: 0, 39 | fields: [ 40 | { 41 | constructor: 0, 42 | fields: [ 43 | { 44 | constructor: 0, 45 | fields: [ 46 | { 47 | constructor: 0, 48 | fields: [ 49 | { 50 | bytes: DatumParameterKey.SenderPubKeyHash 51 | } 52 | ] 53 | }, 54 | { 55 | constructor: 0, 56 | fields: [ 57 | { 58 | constructor: 0, 59 | fields: [ 60 | { 61 | constructor: 0, 62 | fields: [ 63 | { 64 | bytes: DatumParameterKey.SenderStakingKeyHash 65 | } 66 | ] 67 | } 68 | ] 69 | } 70 | ] 71 | } 72 | ] 73 | }, 74 | { 75 | constructor: 1, 76 | fields: [] 77 | } 78 | ] 79 | } 80 | ] 81 | } 82 | ] 83 | } -------------------------------------------------------------------------------- /src/dex/definitions/muesliswap/order.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | export default { 4 | constructor: 0, 5 | fields: [ 6 | { 7 | constructor: 0, 8 | fields: [ 9 | { 10 | constructor: 0, 11 | fields: [ 12 | { 13 | constructor: 0, 14 | fields: [ 15 | { 16 | bytes: DatumParameterKey.SenderPubKeyHash 17 | } 18 | ] 19 | }, 20 | { 21 | constructor: 0, 22 | fields: [ 23 | { 24 | constructor: 0, 25 | fields: [ 26 | { 27 | constructor: 0, 28 | fields: [ 29 | { 30 | bytes: DatumParameterKey.SenderStakingKeyHash 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ] 39 | }, 40 | { 41 | bytes: DatumParameterKey.SwapOutTokenPolicyId 42 | }, 43 | { 44 | bytes: DatumParameterKey.SwapOutTokenAssetName 45 | }, 46 | { 47 | bytes: DatumParameterKey.SwapInTokenPolicyId 48 | }, 49 | { 50 | bytes: DatumParameterKey.SwapInTokenAssetName 51 | }, 52 | { 53 | int: DatumParameterKey.MinReceive 54 | }, 55 | { 56 | constructor: DatumParameterKey.AllowPartialFill, 57 | fields: [] 58 | }, 59 | { 60 | // matchMakerFee + deposit 61 | int: DatumParameterKey.TotalFees 62 | } 63 | ] 64 | } 65 | ] 66 | }; -------------------------------------------------------------------------------- /src/dex/definitions/muesliswap/pool.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | /** 4 | * https://github.com/MuesliSwapTeam/muesliswap-cardano-pool-contracts/blob/main/dex/src/MuesliSwapPools/ConstantProductPool/OnChain.hs 5 | */ 6 | export default { 7 | constructor: 0, 8 | fields: [ 9 | { 10 | constructor: 0, 11 | fields: [ 12 | { 13 | bytes: DatumParameterKey.PoolAssetAPolicyId 14 | }, 15 | { 16 | bytes: DatumParameterKey.PoolAssetAAssetName 17 | } 18 | ] 19 | }, 20 | { 21 | constructor: 0, 22 | fields: [ 23 | { 24 | bytes: DatumParameterKey.PoolAssetBPolicyId 25 | }, 26 | { 27 | bytes: DatumParameterKey.PoolAssetBAssetName 28 | } 29 | ] 30 | }, 31 | { 32 | int: DatumParameterKey.TotalLpTokens 33 | }, 34 | { 35 | int: DatumParameterKey.LpFee 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /src/dex/definitions/splash/order.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | import { DatumParameters, DefinitionField } from '@app/types'; 3 | 4 | export default { 5 | constructor: 0, 6 | fields: [ 7 | { 8 | bytes: DatumParameterKey.Action 9 | }, 10 | { 11 | bytes: DatumParameterKey.Beacon 12 | }, 13 | { 14 | constructor: 0, 15 | fields: [ 16 | { 17 | bytes: DatumParameterKey.SwapInTokenPolicyId 18 | }, 19 | { 20 | bytes: DatumParameterKey.SwapInTokenAssetName 21 | } 22 | ] 23 | }, 24 | { 25 | int: DatumParameterKey.SwapInAmount 26 | }, 27 | { 28 | int: DatumParameterKey.BaseFee 29 | }, 30 | { 31 | int: DatumParameterKey.MinReceive 32 | }, 33 | { 34 | constructor: 0, 35 | fields: [ 36 | { 37 | bytes: DatumParameterKey.SwapOutTokenPolicyId 38 | }, 39 | { 40 | bytes: DatumParameterKey.SwapOutTokenAssetName 41 | } 42 | ] 43 | }, 44 | { 45 | constructor: 0, 46 | fields: [ 47 | { 48 | int: DatumParameterKey.LpFeeNumerator, 49 | }, 50 | { 51 | int: DatumParameterKey.LpFeeDenominator, 52 | } 53 | ] 54 | }, 55 | { 56 | int: DatumParameterKey.ExecutionFee 57 | }, 58 | { 59 | constructor: 0, 60 | fields: [ 61 | { 62 | constructor: 0, 63 | fields: [ 64 | { 65 | bytes: DatumParameterKey.SenderPubKeyHash 66 | } 67 | ] 68 | }, 69 | { 70 | constructor: 0, 71 | fields: [ 72 | { 73 | constructor: 0, 74 | fields: [ 75 | (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { 76 | if (! shouldExtract) { 77 | const stakeKeyHash: string = parameters[DatumParameterKey.SenderStakingKeyHash] as string ?? null; 78 | 79 | if (! stakeKeyHash) return; 80 | 81 | return { 82 | constructor: 0, 83 | fields: [ 84 | { 85 | bytes: stakeKeyHash, 86 | } 87 | ], 88 | }; 89 | } 90 | 91 | if ('fields' in field) { 92 | if (field.constructor === 1) return; 93 | 94 | if (field.fields.length > 0 && 'bytes' in field.fields[0]) { 95 | parameters[DatumParameterKey.SenderStakingKeyHash] = field.fields[0].bytes; 96 | } 97 | } 98 | 99 | return; 100 | }, 101 | ] 102 | } 103 | ] 104 | } 105 | ] 106 | }, 107 | { 108 | bytes: DatumParameterKey.SenderPubKeyHash 109 | }, 110 | [ 111 | { 112 | bytes: DatumParameterKey.Batcher 113 | } 114 | ] 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /src/dex/definitions/splash/pool.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | import { DatumParameters, DefinitionField } from '@app/types'; 3 | 4 | export default { 5 | constructor: 0, 6 | fields: [ 7 | { 8 | constructor: 0, 9 | fields: [ 10 | { 11 | bytes: DatumParameterKey.TokenPolicyId // Pool NFT 12 | }, 13 | { 14 | bytes: DatumParameterKey.TokenAssetName 15 | } 16 | ] 17 | }, 18 | { 19 | constructor: 0, 20 | fields: [ 21 | { 22 | bytes: DatumParameterKey.PoolAssetAPolicyId 23 | }, 24 | { 25 | bytes: DatumParameterKey.PoolAssetAAssetName 26 | } 27 | ] 28 | }, 29 | { 30 | constructor: 0, 31 | fields: [ 32 | { 33 | bytes: DatumParameterKey.PoolAssetBPolicyId 34 | }, 35 | { 36 | bytes: DatumParameterKey.PoolAssetBAssetName 37 | } 38 | ] 39 | }, 40 | { 41 | constructor: 0, 42 | fields: [ 43 | { 44 | bytes: DatumParameterKey.LpTokenPolicyId 45 | }, 46 | { 47 | bytes: DatumParameterKey.LpTokenAssetName 48 | } 49 | ] 50 | }, 51 | { 52 | int: DatumParameterKey.Unknown 53 | }, 54 | { 55 | int: DatumParameterKey.Unknown 56 | }, 57 | { 58 | int: DatumParameterKey.PoolAssetATreasury 59 | }, 60 | { 61 | int: DatumParameterKey.PoolAssetBTreasury 62 | }, 63 | (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { 64 | return; 65 | }, 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /src/dex/definitions/sundaeswap-v1/order.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | export default { 4 | constructor: 0, 5 | fields: [ 6 | { 7 | bytes: DatumParameterKey.PoolIdentifier 8 | }, 9 | { 10 | constructor: 0, 11 | fields: [ 12 | { 13 | constructor: 0, 14 | fields: [ 15 | { 16 | constructor: 0, 17 | fields: [ 18 | { 19 | constructor: 0, 20 | fields: [ 21 | { 22 | bytes: DatumParameterKey.SenderPubKeyHash 23 | } 24 | ] 25 | }, 26 | { 27 | constructor: 0, 28 | fields: [ 29 | { 30 | constructor: 0, 31 | fields: [ 32 | { 33 | constructor: 0, 34 | fields: [ 35 | { 36 | bytes: DatumParameterKey.SenderStakingKeyHash 37 | } 38 | ] 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | ] 45 | }, 46 | { 47 | constructor: 1, 48 | fields: [] 49 | } 50 | ] 51 | }, 52 | { 53 | constructor: 1, 54 | fields: [] 55 | } 56 | ] 57 | }, 58 | { 59 | int: DatumParameterKey.ScooperFee 60 | }, 61 | { 62 | constructor: 0, 63 | fields: [ 64 | { 65 | constructor: DatumParameterKey.Action, 66 | fields: [] 67 | }, 68 | { 69 | int: DatumParameterKey.SwapInAmount 70 | }, 71 | { 72 | constructor: 0, 73 | fields: [ 74 | { 75 | int: DatumParameterKey.MinReceive 76 | } 77 | ] 78 | } 79 | ] 80 | } 81 | ] 82 | } -------------------------------------------------------------------------------- /src/dex/definitions/sundaeswap-v1/pool.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | export default { 4 | constructor: 0, 5 | fields: [ 6 | { 7 | constructor: 0, 8 | fields: [ 9 | { 10 | constructor: 0, 11 | fields: [ 12 | { 13 | bytes: DatumParameterKey.PoolAssetAPolicyId 14 | }, 15 | { 16 | bytes: DatumParameterKey.PoolAssetAAssetName 17 | } 18 | ] 19 | }, 20 | { 21 | constructor: 0, 22 | fields: [ 23 | { 24 | bytes: DatumParameterKey.PoolAssetBPolicyId 25 | }, 26 | { 27 | bytes: DatumParameterKey.PoolAssetBAssetName 28 | } 29 | ] 30 | } 31 | ] 32 | }, 33 | { 34 | bytes: DatumParameterKey.PoolIdentifier 35 | }, 36 | { 37 | int: DatumParameterKey.TotalLpTokens 38 | }, 39 | { 40 | constructor: 0, 41 | fields: [ 42 | { 43 | int: DatumParameterKey.LpFeeNumerator 44 | }, 45 | { 46 | int: DatumParameterKey.LpFeeDenominator 47 | } 48 | ] 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /src/dex/definitions/sundaeswap-v3/order.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | export default { 4 | constructor: 0, 5 | fields: [ 6 | { 7 | constructor: 0, 8 | fields: [ 9 | { 10 | bytes: DatumParameterKey.PoolIdentifier, 11 | }, 12 | ], 13 | }, 14 | { 15 | constructor: 0, 16 | fields: [ 17 | { 18 | bytes: DatumParameterKey.SenderStakingKeyHash, 19 | }, 20 | ], 21 | }, 22 | { 23 | int: DatumParameterKey.ProtocolFee, 24 | }, 25 | { 26 | constructor: 0, 27 | fields: [ 28 | { 29 | constructor: 0, 30 | fields: [ 31 | { 32 | constructor: 0, 33 | fields: [ 34 | { 35 | bytes: DatumParameterKey.SenderPubKeyHash, 36 | }, 37 | ], 38 | }, 39 | { 40 | constructor: 0, 41 | fields: [ 42 | { 43 | constructor: 0, 44 | fields: [ 45 | { 46 | constructor: 0, 47 | fields: [ 48 | { 49 | bytes: DatumParameterKey.SenderStakingKeyHash, 50 | }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | ], 56 | }, 57 | ], 58 | }, 59 | { 60 | constructor: 0, 61 | fields: [], 62 | }, 63 | ], 64 | }, 65 | { 66 | constructor: 1, 67 | fields: [ 68 | [ 69 | { 70 | bytes: DatumParameterKey.SwapInTokenPolicyId, 71 | }, 72 | { 73 | bytes: DatumParameterKey.SwapInTokenAssetName, 74 | }, 75 | { 76 | int: DatumParameterKey.SwapInAmount, 77 | }, 78 | ], 79 | [ 80 | { 81 | bytes: DatumParameterKey.SwapOutTokenPolicyId, 82 | }, 83 | { 84 | bytes: DatumParameterKey.SwapOutTokenAssetName, 85 | }, 86 | { 87 | int: DatumParameterKey.MinReceive, 88 | }, 89 | ], 90 | ], 91 | }, 92 | { 93 | bytes: DatumParameterKey.CancelDatum, 94 | }, 95 | ], 96 | }; 97 | -------------------------------------------------------------------------------- /src/dex/definitions/sundaeswap-v3/pool.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | import { DatumParameters, DefinitionField } from '@app/types'; 3 | 4 | export default { 5 | constructor: 0, 6 | fields: [ 7 | { 8 | bytes: DatumParameterKey.PoolIdentifier, 9 | }, 10 | { 11 | list: [ 12 | { 13 | list: [ 14 | { 15 | bytes: DatumParameterKey.PoolAssetAPolicyId 16 | }, 17 | { 18 | bytes: DatumParameterKey.PoolAssetAAssetName 19 | } 20 | ], 21 | }, 22 | { 23 | list: [ 24 | { 25 | bytes: DatumParameterKey.PoolAssetBPolicyId 26 | }, 27 | { 28 | bytes: DatumParameterKey.PoolAssetBAssetName 29 | } 30 | ], 31 | }, 32 | ], 33 | }, 34 | { 35 | int: DatumParameterKey.TotalLpTokens 36 | }, 37 | { 38 | int: DatumParameterKey.OpeningFee 39 | }, 40 | { 41 | int: DatumParameterKey.FinalFee 42 | }, 43 | (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { 44 | return; 45 | }, 46 | ], 47 | }; 48 | -------------------------------------------------------------------------------- /src/dex/definitions/vyfinance/order.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | export default { 4 | constructor: 0, 5 | fields: [ 6 | { 7 | bytes: DatumParameterKey.SenderKeyHashes 8 | }, 9 | { 10 | constructor: DatumParameterKey.Action, 11 | fields: [ 12 | { 13 | int: DatumParameterKey.MinReceive 14 | } 15 | ] 16 | } 17 | ] 18 | }; -------------------------------------------------------------------------------- /src/dex/definitions/vyfinance/pool.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | export default { 4 | constructor: 0, 5 | fields: [ 6 | { 7 | int: DatumParameterKey.PoolAssetABarFee 8 | }, 9 | { 10 | int: DatumParameterKey.PoolAssetBBarFee 11 | }, 12 | { 13 | int: DatumParameterKey.TotalLpTokens 14 | } 15 | ] 16 | }; -------------------------------------------------------------------------------- /src/dex/definitions/wingriders-v2/order.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | export default { 4 | constructor: 0, 5 | fields: [ 6 | { 7 | int: DatumParameterKey.DepositFee 8 | }, 9 | { 10 | constructor: 0, 11 | fields: [ 12 | { 13 | constructor: 0, 14 | fields: [ 15 | { 16 | bytes: DatumParameterKey.ReceiverPubKeyHash 17 | } 18 | ] 19 | }, 20 | { 21 | constructor: 0, 22 | fields: [ 23 | { 24 | constructor: 0, 25 | fields: [ 26 | { 27 | constructor: 0, 28 | fields: [ 29 | { 30 | bytes: DatumParameterKey.ReceiverStakingKeyHash 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ] 39 | }, 40 | { 41 | constructor: 0, 42 | fields: [ 43 | { 44 | constructor: 0, 45 | fields: [ 46 | { 47 | bytes: DatumParameterKey.ReceiverPubKeyHash 48 | } 49 | ] 50 | }, 51 | { 52 | constructor: 0, 53 | fields: [ 54 | { 55 | constructor: 0, 56 | fields: [ 57 | { 58 | constructor: 0, 59 | fields: [ 60 | { 61 | bytes: DatumParameterKey.ReceiverStakingKeyHash 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | ] 68 | } 69 | ] 70 | }, 71 | [], 72 | { 73 | constructor: 0, 74 | fields: [] 75 | }, 76 | { 77 | int: DatumParameterKey.Expiration 78 | }, 79 | { 80 | bytes: DatumParameterKey.PoolAssetAPolicyId 81 | }, 82 | { 83 | bytes: DatumParameterKey.PoolAssetAAssetName 84 | }, 85 | { 86 | bytes: DatumParameterKey.PoolAssetBPolicyId 87 | }, 88 | { 89 | bytes: DatumParameterKey.PoolAssetBAssetName 90 | }, 91 | { 92 | constructor: 0, 93 | fields: [ 94 | { 95 | constructor: DatumParameterKey.Action, 96 | fields: [] 97 | }, 98 | { 99 | int: DatumParameterKey.MinReceive 100 | } 101 | ] 102 | }, 103 | { 104 | int: DatumParameterKey.AScale 105 | }, 106 | { 107 | int: DatumParameterKey.BScale 108 | } 109 | ] 110 | }; -------------------------------------------------------------------------------- /src/dex/definitions/wingriders-v2/pool.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | import { DatumParameters, DefinitionField } from '@app/types'; 3 | 4 | export default { 5 | constructor: 0, 6 | fields: [ 7 | { 8 | bytes: DatumParameterKey.RequestScriptHash 9 | }, 10 | { 11 | bytes: DatumParameterKey.PoolAssetAPolicyId, 12 | }, 13 | { 14 | bytes: DatumParameterKey.PoolAssetAAssetName, 15 | }, 16 | { 17 | bytes: DatumParameterKey.PoolAssetBPolicyId, 18 | }, 19 | { 20 | bytes: DatumParameterKey.PoolAssetBAssetName, 21 | }, 22 | { 23 | int: DatumParameterKey.SwapFee 24 | }, 25 | { 26 | int: DatumParameterKey.ProtocolFee 27 | }, 28 | { 29 | int: DatumParameterKey.ProjectFeeInBasis 30 | }, 31 | { 32 | int: DatumParameterKey.ReserveFeeInBasis 33 | }, 34 | { 35 | int: DatumParameterKey.FeeBasis 36 | }, 37 | { 38 | int: DatumParameterKey.AgentFee 39 | }, 40 | { 41 | int: DatumParameterKey.LastInteraction 42 | }, 43 | { 44 | int: DatumParameterKey.PoolAssetATreasury 45 | }, 46 | { 47 | int: DatumParameterKey.PoolAssetBTreasury 48 | }, 49 | { 50 | int: DatumParameterKey.Unknown 51 | }, 52 | { 53 | int: DatumParameterKey.Unknown 54 | }, 55 | { 56 | int: DatumParameterKey.Unknown 57 | }, 58 | { 59 | int: DatumParameterKey.Unknown 60 | }, 61 | (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { 62 | return parameters; 63 | }, 64 | (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { 65 | return parameters; 66 | }, 67 | (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { 68 | return parameters; 69 | }, 70 | ] 71 | }; -------------------------------------------------------------------------------- /src/dex/definitions/wingriders/order.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | /** 4 | * https://github.com/WingRiders/dex-serializer/blob/main/src/RequestDatum.ts 5 | */ 6 | export default { 7 | constructor: 0, 8 | fields: [ 9 | { 10 | constructor: 0, 11 | fields: [ 12 | { 13 | constructor: 0, 14 | fields: [ 15 | { 16 | constructor: 0, 17 | fields: [ 18 | { 19 | bytes: DatumParameterKey.ReceiverPubKeyHash 20 | } 21 | ] 22 | }, 23 | { 24 | constructor: 0, 25 | fields: [ 26 | { 27 | constructor: 0, 28 | fields: [ 29 | { 30 | constructor: 0, 31 | fields: [ 32 | { 33 | bytes: DatumParameterKey.ReceiverStakingKeyHash 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | { 44 | bytes: DatumParameterKey.SenderPubKeyHash 45 | }, 46 | { 47 | int: DatumParameterKey.Expiration 48 | }, 49 | { 50 | constructor: 0, 51 | fields: [ 52 | { 53 | constructor: 0, 54 | fields: [ 55 | { 56 | bytes: DatumParameterKey.PoolAssetAPolicyId 57 | }, 58 | { 59 | bytes: DatumParameterKey.PoolAssetAAssetName 60 | } 61 | ] 62 | }, 63 | { 64 | constructor: 0, 65 | fields: [ 66 | { 67 | bytes: DatumParameterKey.PoolAssetBPolicyId 68 | }, 69 | { 70 | bytes: DatumParameterKey.PoolAssetBAssetName 71 | } 72 | ] 73 | } 74 | ] 75 | } 76 | ] 77 | }, 78 | { 79 | constructor: 0, 80 | fields: [ 81 | { 82 | constructor: DatumParameterKey.Action, 83 | fields: [] 84 | }, 85 | { 86 | int: DatumParameterKey.MinReceive 87 | } 88 | ] 89 | } 90 | ] 91 | }; -------------------------------------------------------------------------------- /src/dex/definitions/wingriders/pool.ts: -------------------------------------------------------------------------------- 1 | import { DatumParameterKey } from '@app/constants'; 2 | 3 | /** 4 | * https://github.com/WingRiders/dex-serializer/blob/main/src/LiquidityPoolDatum.ts 5 | */ 6 | export default { 7 | constructor: 0, 8 | fields: [ 9 | { 10 | bytes: DatumParameterKey.RequestScriptHash 11 | }, 12 | { 13 | constructor: 0, 14 | fields: [ 15 | { 16 | constructor: 0, 17 | fields: [ 18 | { 19 | constructor: 0, 20 | fields: [ 21 | { 22 | bytes: DatumParameterKey.PoolAssetAPolicyId, 23 | }, 24 | { 25 | bytes: DatumParameterKey.PoolAssetAAssetName, 26 | } 27 | ] 28 | }, 29 | { 30 | constructor: 0, 31 | fields: [ 32 | { 33 | bytes: DatumParameterKey.PoolAssetBPolicyId, 34 | }, 35 | { 36 | bytes: DatumParameterKey.PoolAssetBAssetName, 37 | } 38 | ] 39 | } 40 | ] 41 | }, 42 | { 43 | int: DatumParameterKey.LastInteraction, 44 | }, 45 | { 46 | int: DatumParameterKey.PoolAssetATreasury, 47 | }, 48 | { 49 | int: DatumParameterKey.PoolAssetBTreasury, 50 | } 51 | ] 52 | } 53 | ] 54 | }; -------------------------------------------------------------------------------- /src/dex/logo/minswap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndigoProtocol/dexter/78ea53d408617409164a63b8ed25e57955dfc715/src/dex/logo/minswap.png -------------------------------------------------------------------------------- /src/dex/logo/minswapv2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndigoProtocol/dexter/78ea53d408617409164a63b8ed25e57955dfc715/src/dex/logo/minswapv2.png -------------------------------------------------------------------------------- /src/dex/logo/muesliswap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndigoProtocol/dexter/78ea53d408617409164a63b8ed25e57955dfc715/src/dex/logo/muesliswap.png -------------------------------------------------------------------------------- /src/dex/logo/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndigoProtocol/dexter/78ea53d408617409164a63b8ed25e57955dfc715/src/dex/logo/splash.png -------------------------------------------------------------------------------- /src/dex/logo/sundaeswap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndigoProtocol/dexter/78ea53d408617409164a63b8ed25e57955dfc715/src/dex/logo/sundaeswap.png -------------------------------------------------------------------------------- /src/dex/logo/vyfinance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndigoProtocol/dexter/78ea53d408617409164a63b8ed25e57955dfc715/src/dex/logo/vyfinance.png -------------------------------------------------------------------------------- /src/dex/logo/wingriders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndigoProtocol/dexter/78ea53d408617409164a63b8ed25e57955dfc715/src/dex/logo/wingriders.png -------------------------------------------------------------------------------- /src/dex/logo/wingridersv2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndigoProtocol/dexter/78ea53d408617409164a63b8ed25e57955dfc715/src/dex/logo/wingridersv2.png -------------------------------------------------------------------------------- /src/dex/models/asset.ts: -------------------------------------------------------------------------------- 1 | export class Asset { 2 | 3 | public policyId: string; 4 | public nameHex: string; 5 | public decimals: number; 6 | 7 | constructor(policyId: string, nameHex: string, decimals: number = 0) { 8 | this.policyId = policyId; 9 | this.nameHex = nameHex; 10 | this.decimals = decimals; 11 | } 12 | 13 | static fromIdentifier(id: string, decimals: number = 0): Asset { 14 | id = id.replace('.', ''); 15 | 16 | return new Asset( 17 | id.slice(0, 56), 18 | id.slice(56), 19 | decimals, 20 | ); 21 | } 22 | 23 | identifier(dilimeter: '' | '.' = ''): string { 24 | return this.policyId + dilimeter + this.nameHex; 25 | } 26 | 27 | get assetName(): string { 28 | return Buffer.from(this.nameHex, 'hex').toString(); 29 | } 30 | 31 | } 32 | 33 | export type Token = Asset | 'lovelace'; 34 | -------------------------------------------------------------------------------- /src/dex/models/dex-transaction.ts: -------------------------------------------------------------------------------- 1 | import { TransactionStatus } from '@app/constants'; 2 | import { BaseWalletProvider } from '@providers/wallet/base-wallet-provider'; 3 | import { DexTransactionError, PayToAddress } from '@app/types'; 4 | 5 | interface TransactionCallback { 6 | (transaction: DexTransaction): void; 7 | } 8 | 9 | export class DexTransaction { 10 | 11 | public providerData: any = {}; 12 | public error: DexTransactionError | undefined = undefined; 13 | 14 | private _hash: string; 15 | private _isSigned: boolean = false; 16 | private _payments: PayToAddress[] = []; 17 | private _walletProvider: BaseWalletProvider; 18 | private _currentStatus: TransactionStatus = TransactionStatus.Building; 19 | private _listeners: TransactionCallback[] = []; 20 | 21 | constructor(walletProvider: BaseWalletProvider) { 22 | this._walletProvider = walletProvider; 23 | } 24 | 25 | get hash(): string { 26 | return this._hash; 27 | } 28 | 29 | get isSigned(): boolean { 30 | return this._isSigned; 31 | } 32 | 33 | get payments(): PayToAddress[] { 34 | return this._payments; 35 | } 36 | 37 | get status(): TransactionStatus { 38 | return this._currentStatus; 39 | } 40 | 41 | set status(status: TransactionStatus) { 42 | this._currentStatus = status; 43 | 44 | this._listeners.forEach((callback: TransactionCallback) => { 45 | callback(this); 46 | }); 47 | } 48 | 49 | public attachMetadata(key: number, json: Object): DexTransaction { 50 | return this._walletProvider.attachMetadata(this, key, json); 51 | } 52 | 53 | public payToAddresses(payToAddresses: PayToAddress[]): Promise { 54 | return this._walletProvider.paymentsForTransaction(this, payToAddresses) 55 | .then(() => { 56 | this._payments = payToAddresses; 57 | 58 | return this as DexTransaction; 59 | }); 60 | } 61 | 62 | public sign(): Promise { 63 | if (this._isSigned) { 64 | throw new Error('Transaction was already signed.'); 65 | } 66 | 67 | return this._walletProvider.signTransaction(this) 68 | .then(() => { 69 | this._isSigned = true; 70 | 71 | return this as DexTransaction; 72 | }); 73 | } 74 | 75 | public submit(): Promise { 76 | if (! this._isSigned) { 77 | throw new Error('Must sign transaction before submitting.'); 78 | } 79 | if (this._hash) { 80 | throw new Error('Transaction was already submitted.'); 81 | } 82 | 83 | return this._walletProvider.submitTransaction(this) 84 | .then((txHash: string) => { 85 | this._hash = txHash; 86 | 87 | return this as DexTransaction; 88 | }); 89 | } 90 | 91 | public onBuilding(callback: TransactionCallback): DexTransaction { 92 | this.addListener((transaction: DexTransaction) => { 93 | if (transaction.status === TransactionStatus.Building) { 94 | callback(transaction); 95 | } 96 | }); 97 | 98 | return this; 99 | } 100 | 101 | public onSigning(callback: TransactionCallback): DexTransaction { 102 | this.addListener((transaction: DexTransaction) => { 103 | if (transaction.status === TransactionStatus.Signing) { 104 | callback(transaction); 105 | } 106 | }); 107 | 108 | return this; 109 | } 110 | 111 | public onSubmitting(callback: TransactionCallback): DexTransaction { 112 | this.addListener((transaction: DexTransaction) => { 113 | if (transaction.status === TransactionStatus.Submitting) { 114 | callback(transaction); 115 | } 116 | }); 117 | 118 | return this; 119 | } 120 | 121 | public onSubmitted(callback: TransactionCallback): DexTransaction { 122 | this.addListener((transaction: DexTransaction) => { 123 | if (transaction.status === TransactionStatus.Submitted) { 124 | callback(transaction); 125 | } 126 | }); 127 | 128 | return this; 129 | } 130 | 131 | public onError(callback: TransactionCallback): DexTransaction { 132 | this.addListener((transaction: DexTransaction) => { 133 | if (transaction.status === TransactionStatus.Errored) { 134 | callback(transaction); 135 | } 136 | }); 137 | 138 | return this; 139 | } 140 | 141 | public onFinally(callback: TransactionCallback): DexTransaction { 142 | this.addListener((transaction: DexTransaction) => { 143 | if (transaction.status === TransactionStatus.Submitted || transaction.status === TransactionStatus.Errored) { 144 | callback(transaction); 145 | } 146 | }); 147 | 148 | return this; 149 | } 150 | 151 | private addListener(callback: TransactionCallback): void { 152 | this._listeners.push(callback); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/dex/models/liquidity-pool.ts: -------------------------------------------------------------------------------- 1 | import { Asset, Token } from './asset'; 2 | 3 | export class LiquidityPool { 4 | 5 | dex: string; 6 | assetA: Token; 7 | assetB: Token; 8 | reserveA: bigint; 9 | reserveB: bigint; 10 | address: string; 11 | marketOrderAddress: string; 12 | limitOrderAddress: string; 13 | 14 | lpToken: Asset; 15 | poolNft: Asset; 16 | identifier: string = ''; 17 | poolFeePercent: number = 0; 18 | totalLpTokens: bigint = 0n; 19 | extra: any = {}; 20 | 21 | constructor(dex: string, assetA: Token, assetB: Token, reserveA: bigint, reserveB: bigint, address: string, marketOrderAddress: string = '', limitOrderAddress: string = '') { 22 | this.dex = dex; 23 | this.assetA = assetA; 24 | this.assetB = assetB; 25 | this.reserveA = reserveA; 26 | this.reserveB = reserveB; 27 | this.address = address; 28 | this.marketOrderAddress = marketOrderAddress; 29 | this.limitOrderAddress = limitOrderAddress; 30 | } 31 | 32 | get uuid(): string { 33 | return `${this.dex}.${this.pair}.${this.identifier}`; 34 | } 35 | 36 | get pair(): string { 37 | const assetAName: string = this.assetA === 'lovelace' ? 'ADA' : this.assetA.assetName; 38 | const assetBName: string = this.assetB === 'lovelace' ? 'ADA' : this.assetB.assetName; 39 | 40 | return `${assetAName}/${assetBName}`; 41 | } 42 | 43 | get price(): number { 44 | const assetADecimals: number = this.assetA === 'lovelace' ? 6 : this.assetA.decimals; 45 | const assetBDecimals: number = this.assetB === 'lovelace' ? 6 : this.assetB.decimals; 46 | 47 | const adjustedReserveA: number = Number(this.reserveA) / (10**assetADecimals); 48 | const adjustedReserveB: number = Number(this.reserveB) / (10**assetBDecimals); 49 | 50 | return adjustedReserveA / adjustedReserveB; 51 | } 52 | 53 | get totalValueLocked(): number { 54 | const assetADecimals: number = this.assetA === 'lovelace' ? 6 : this.assetA.decimals; 55 | const assetBDecimals: number = this.assetB === 'lovelace' ? 6 : this.assetB.decimals; 56 | 57 | if (this.assetA === 'lovelace') { 58 | return (Number(this.reserveA) / 10**assetADecimals) + ((Number(this.reserveB) / 10**assetBDecimals) * this.price); 59 | } 60 | 61 | return ((Number(this.reserveA) / 10**assetADecimals) * this.price) * ((Number(this.reserveB) / 10**assetBDecimals) * (1 / this.price)); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/dexter.ts: -------------------------------------------------------------------------------- 1 | import { BaseDataProvider } from '@providers/data/base-data-provider'; 2 | import { AvailableDexs, DexterConfig, RequestConfig } from '@app/types'; 3 | import { Minswap } from '@dex/minswap'; 4 | import { SundaeSwapV1 } from '@dex/sundaeswap-v1'; 5 | import { MuesliSwap } from '@dex/muesliswap'; 6 | import { WingRiders } from '@dex/wingriders'; 7 | import { SwapRequest } from '@requests/swap-request'; 8 | import { BaseWalletProvider } from '@providers/wallet/base-wallet-provider'; 9 | import { BaseDex } from '@dex/base-dex'; 10 | import { VyFinance } from '@dex/vyfinance'; 11 | import { BaseMetadataProvider } from '@providers/asset-metadata/base-metadata-provider'; 12 | import { TokenRegistryProvider } from '@providers/asset-metadata/token-registry-provider'; 13 | import { CancelSwapRequest } from '@requests/cancel-swap-request'; 14 | import { FetchRequest } from '@requests/fetch-request'; 15 | import axios from 'axios'; 16 | import axiosRetry from 'axios-retry'; 17 | import { SplitSwapRequest } from '@requests/split-swap-request'; 18 | import { SplitCancelSwapRequest } from '@requests/split-cancel-swap-request'; 19 | import { SundaeSwapV3 } from '@dex/sundaeswap-v3'; 20 | import { MinswapV2 } from '@dex/minswap-v2'; 21 | import { WingRidersV2 } from '@dex/wingriders-v2'; 22 | import { Splash } from '@dex/splash'; 23 | 24 | export class Dexter { 25 | 26 | public config: DexterConfig; 27 | public requestConfig: RequestConfig; 28 | 29 | public dataProvider?: BaseDataProvider; 30 | public walletProvider?: BaseWalletProvider; 31 | public metadataProvider: BaseMetadataProvider; 32 | 33 | public availableDexs: AvailableDexs; 34 | 35 | constructor(config: DexterConfig = {}, requestConfig: RequestConfig = {}) { 36 | this.config = Object.assign( 37 | {}, 38 | { 39 | shouldFetchMetadata: true, 40 | shouldFallbackToApi: true, 41 | shouldSubmitOrders: false, 42 | metadataMsgBranding: 'Dexter', 43 | } as DexterConfig, 44 | config, 45 | ); 46 | this.requestConfig = Object.assign( 47 | {}, 48 | { 49 | timeout: 5000, 50 | proxyUrl: '', 51 | retries: 3, 52 | } as RequestConfig, 53 | requestConfig, 54 | ); 55 | 56 | // Axios configurations 57 | axiosRetry(axios, { retries: this.requestConfig.retries }); 58 | axios.defaults.timeout = this.requestConfig.timeout; 59 | 60 | this.metadataProvider = new TokenRegistryProvider(this.requestConfig); 61 | this.availableDexs = { 62 | [Minswap.identifier]: new Minswap(this.requestConfig), 63 | [SundaeSwapV1.identifier]: new SundaeSwapV1(this.requestConfig), 64 | [SundaeSwapV3.identifier]: new SundaeSwapV3(this.requestConfig), 65 | [MinswapV2.identifier]: new MinswapV2(this.requestConfig), 66 | [MuesliSwap.identifier]: new MuesliSwap(this.requestConfig), 67 | [WingRiders.identifier]: new WingRiders(this.requestConfig), 68 | [WingRidersV2.identifier]: new WingRidersV2(this.requestConfig), 69 | [VyFinance.identifier]: new VyFinance(this.requestConfig), 70 | [Splash.identifier]: new Splash(this.requestConfig), 71 | }; 72 | } 73 | 74 | /** 75 | * Retrieve DEX instance from unique name. 76 | */ 77 | public dexByName(name: string): BaseDex | undefined { 78 | return this.availableDexs[name]; 79 | } 80 | 81 | /** 82 | * Switch to a new data provider. 83 | */ 84 | public withDataProvider(dataProvider: BaseDataProvider): Dexter { 85 | this.dataProvider = dataProvider; 86 | 87 | return this; 88 | } 89 | 90 | /** 91 | * Switch to a new wallet provider. 92 | */ 93 | public withWalletProvider(walletProvider: BaseWalletProvider): Dexter { 94 | this.walletProvider = walletProvider; 95 | 96 | return this; 97 | } 98 | 99 | /** 100 | * Switch to a new asset metadata provider. 101 | */ 102 | public withMetadataProvider(metadataProvider: BaseMetadataProvider): Dexter { 103 | this.metadataProvider = metadataProvider; 104 | 105 | return this; 106 | } 107 | 108 | /** 109 | * New request for data fetching. 110 | */ 111 | public newFetchRequest(): FetchRequest { 112 | return new FetchRequest(this); 113 | } 114 | 115 | /** 116 | * New request for a swap order. 117 | */ 118 | public newSwapRequest(): SwapRequest { 119 | return new SwapRequest(this); 120 | } 121 | 122 | /** 123 | * New request for a split swap order. 124 | */ 125 | public newSplitSwapRequest(): SplitSwapRequest { 126 | return new SplitSwapRequest(this); 127 | } 128 | 129 | /** 130 | * New request for cancelling a swap order. 131 | */ 132 | public newCancelSwapRequest(): CancelSwapRequest { 133 | if (! this.walletProvider) { 134 | throw new Error('Wallet provider must be set before requesting a cancel order.'); 135 | } 136 | if (! this.walletProvider.isWalletLoaded) { 137 | throw new Error('Wallet must be loaded before requesting a cancel order.'); 138 | } 139 | 140 | return new CancelSwapRequest(this); 141 | } 142 | 143 | /** 144 | * New request for a split cancel swap order. 145 | */ 146 | public newSplitCancelSwapRequest(): SplitCancelSwapRequest { 147 | if (! this.walletProvider) { 148 | throw new Error('Wallet provider must be set before requesting a split cancel order.'); 149 | } 150 | if (! this.walletProvider.isWalletLoaded) { 151 | throw new Error('Wallet must be loaded before requesting a split cancel order.'); 152 | } 153 | 154 | return new SplitCancelSwapRequest(this); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base exports. 3 | */ 4 | export * from './constants'; 5 | export * from './types'; 6 | export * from './utils'; 7 | export * from './dexter'; 8 | export * from './definition-builder'; 9 | 10 | /** 11 | * Provider exports. 12 | */ 13 | export * from './providers/data/base-data-provider'; 14 | export * from './providers/data/blockfrost-provider'; 15 | export * from './providers/data/kupo-provider'; 16 | export * from './providers/data/mock-data-provider'; 17 | 18 | export * from './providers/wallet/base-wallet-provider'; 19 | export * from './providers/wallet/mock-wallet-provider'; 20 | export * from './providers/wallet/lucid-provider'; 21 | 22 | export * from './providers/asset-metadata/base-metadata-provider'; 23 | export * from './providers/asset-metadata/token-registry-provider'; 24 | 25 | /** 26 | * Request exports. 27 | */ 28 | export * from './requests/fetch-request'; 29 | export * from './requests/swap-request'; 30 | export * from './requests/split-swap-request'; 31 | export * from './requests/cancel-swap-request'; 32 | export * from './requests/split-cancel-swap-request'; 33 | 34 | /** 35 | * DEX exports. 36 | */ 37 | export * from './dex/models/asset'; 38 | export * from './dex/models/liquidity-pool'; 39 | export * from './dex/models/dex-transaction'; 40 | 41 | export * from './dex/base-dex'; 42 | export * from './dex/minswap'; 43 | export * from './dex/minswap-v2'; 44 | export * from './dex/sundaeswap-v1'; 45 | export * from './dex/sundaeswap-v3'; 46 | export * from './dex/muesliswap'; 47 | export * from './dex/wingriders'; 48 | export * from './dex/wingriders-v2'; 49 | export * from './dex/vyfinance'; 50 | export * from './dex/splash'; 51 | -------------------------------------------------------------------------------- /src/providers/asset-metadata/base-metadata-provider.ts: -------------------------------------------------------------------------------- 1 | import { AssetMetadata } from '@app/types'; 2 | import { Asset } from '@dex/models/asset'; 3 | 4 | export abstract class BaseMetadataProvider { 5 | 6 | /** 7 | * Fetch Asset metadata. 8 | */ 9 | abstract fetch(assets: Asset[]): Promise; 10 | 11 | } -------------------------------------------------------------------------------- /src/providers/asset-metadata/token-registry-provider.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from 'axios'; 2 | import { AssetMetadata, RequestConfig } from '@app/types'; 3 | import { Asset } from '@dex/models/asset'; 4 | import { BaseMetadataProvider } from './base-metadata-provider'; 5 | import { appendSlash } from '@app/utils'; 6 | 7 | export class TokenRegistryProvider extends BaseMetadataProvider { 8 | 9 | private _api: AxiosInstance; 10 | private _requestConfig: RequestConfig; 11 | 12 | /** 13 | * https://input-output-hk.github.io/offchain-metadata-tools/api/latest/ 14 | */ 15 | constructor(requestConfig: RequestConfig = {}) { 16 | super(); 17 | 18 | this._requestConfig = requestConfig; 19 | 20 | this._api = axios.create({ 21 | timeout: requestConfig.timeout ?? 5000, 22 | baseURL: `${appendSlash(requestConfig.proxyUrl)}https://tokens.cardano.org/`, 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | }, 26 | }); 27 | } 28 | 29 | /** 30 | * https://input-output-hk.github.io/offchain-metadata-tools/api/latest/#tag/query/paths/~1metadata~1query/post 31 | */ 32 | fetch(assets: Asset[]): Promise { 33 | return this._api.post('/metadata/query', { 34 | subjects: assets.map((asset: Asset) => asset.identifier()), 35 | }).then((response) => response.data.subjects.map((entry: any) => { 36 | return { 37 | policyId: entry.subject.slice(0, 56), 38 | nameHex: entry.subject.slice(56), 39 | decimals: entry.decimals ? Number(entry.decimals.value) : 0, 40 | } as AssetMetadata; 41 | })); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/providers/data/base-data-provider.ts: -------------------------------------------------------------------------------- 1 | import { AssetAddress, DefinitionField, Transaction, UTxO } from '@app/types'; 2 | import { Asset } from '@dex/models/asset'; 3 | 4 | export abstract class BaseDataProvider { 5 | 6 | /** 7 | * Fetch all UTxOs for an address. Will filter on UTxOs containing 8 | * assetId (concatenation of policy ID & asset name) if provided. 9 | */ 10 | abstract utxos(address: string, asset?: Asset): Promise; 11 | 12 | /** 13 | * Fetch all UTxOs for a transaction. 14 | */ 15 | abstract transactionUtxos(txHash: string): Promise; 16 | 17 | /** 18 | * Fetch all transactions containing and asset. 19 | */ 20 | abstract assetTransactions(asset: Asset): Promise; 21 | 22 | /** 23 | * Fetch all addresses containing an asset. 24 | */ 25 | abstract assetAddresses(asset: Asset): Promise 26 | 27 | /** 28 | * Fetch JSON value of a datum by its hash. 29 | */ 30 | abstract datumValue(datumHash: string): Promise; 31 | 32 | } -------------------------------------------------------------------------------- /src/providers/data/blockfrost-provider.ts: -------------------------------------------------------------------------------- 1 | import { BaseDataProvider } from './base-data-provider'; 2 | import axios, { AxiosInstance } from 'axios'; 3 | import { 4 | AssetAddress, 5 | AssetBalance, 6 | BlockfrostConfig, 7 | DefinitionField, 8 | RequestConfig, 9 | Transaction, 10 | UTxO 11 | } from '@app/types'; 12 | import { Asset } from '@dex/models/asset'; 13 | import Bottleneck from 'bottleneck'; 14 | import { appendSlash } from '@app/utils'; 15 | 16 | const API_BURST_SIZE: number = 500; 17 | const API_COOLDOWN_SIZE: number = 10; 18 | const API_COOLDOWN_MS: number = 1000; 19 | 20 | export class BlockfrostProvider extends BaseDataProvider { 21 | 22 | private _api: AxiosInstance; 23 | private _requestConfig: RequestConfig; 24 | private _limiter: Bottleneck; 25 | 26 | /** 27 | * https://docs.blockfrost.io/ 28 | */ 29 | constructor(config: BlockfrostConfig, requestConfig: RequestConfig = {}) { 30 | super(); 31 | 32 | this._requestConfig = Object.assign( 33 | {}, 34 | { 35 | timeout: 5000, 36 | proxyUrl: '', 37 | } as RequestConfig, 38 | requestConfig, 39 | ); 40 | 41 | this._api = axios.create({ 42 | baseURL: (appendSlash(requestConfig.proxyUrl)) + config.url, 43 | timeout: this._requestConfig.timeout, 44 | headers: { 45 | 'Content-Type': 'application/json', 46 | project_id: config.projectId, 47 | }, 48 | }); 49 | 50 | // https://docs.blockfrost.io/#section/Limits 51 | this._limiter = new Bottleneck({ 52 | reservoir: API_BURST_SIZE, 53 | reservoirIncreaseAmount: API_COOLDOWN_SIZE, 54 | reservoirIncreaseInterval: API_COOLDOWN_MS, 55 | reservoirIncreaseMaximum: API_BURST_SIZE, 56 | }); 57 | } 58 | 59 | /** 60 | * https://docs.blockfrost.io/#tag/Cardano-Addresses/paths/~1addresses~1%7Baddress%7D~1utxos/get 61 | * https://docs.blockfrost.io/#tag/Cardano-Addresses/paths/~1addresses~1%7Baddress%7D~1utxos~1%7Basset%7D/get 62 | */ 63 | public async utxos(address: string, asset?: Asset): Promise { 64 | return this.sendPaginatedRequest(`/addresses/${address}/utxos/${asset ? asset.identifier() : ''}`) 65 | .then((results: any) => { 66 | return results.map((utxo: any) => { 67 | return { 68 | txHash: utxo.tx_hash, 69 | address: utxo.address, 70 | datumHash: utxo.data_hash, 71 | outputIndex: utxo.output_index, 72 | assetBalances: utxo.amount.reduce((assets: AssetBalance[], amount: any) => { 73 | assets.push({ 74 | asset: amount.unit === 'lovelace' ? amount.unit : Asset.fromIdentifier(amount.unit), 75 | quantity: BigInt(amount.quantity), 76 | }) 77 | return assets; 78 | }, []), 79 | } as UTxO; 80 | }) as UTxO[]; 81 | }); 82 | } 83 | 84 | /** 85 | * https://docs.blockfrost.io/#tag/Cardano-Transactions/paths/~1txs~1%7Bhash%7D~1utxos/get 86 | */ 87 | public async transactionUtxos(txHash: string): Promise { 88 | return this._limiter.schedule(() => this._api.get(`/txs/${txHash}/utxos`)) 89 | .then((response: any) => { 90 | return response.data.outputs.map((utxo: any) => { 91 | return { 92 | txHash: response.data.hash, 93 | address: utxo.address, 94 | datumHash: utxo.data_hash, 95 | datum: utxo.inline_datum, 96 | outputIndex: utxo.output_index, 97 | assetBalances: utxo.amount.reduce((assets: AssetBalance[], amount: any) => { 98 | assets.push({ 99 | asset: amount.unit === 'lovelace' ? amount.unit : Asset.fromIdentifier(amount.unit), 100 | quantity: BigInt(amount.quantity), 101 | }) 102 | return assets; 103 | }, []), 104 | } as UTxO; 105 | }) as UTxO[]; 106 | }); 107 | } 108 | 109 | /** 110 | * https://docs.blockfrost.io/#tag/Cardano-Assets/paths/~1assets~1%7Basset%7D~1transactions/get 111 | */ 112 | public async assetTransactions(asset: Asset): Promise { 113 | return this.sendPaginatedRequest(`/assets/${asset.identifier()}/transactions`) 114 | .then((results: any) => { 115 | return results.map((tx: any) => { 116 | return { 117 | hash: tx.tx_hash, 118 | } as Transaction; 119 | }) as Transaction[]; 120 | }); 121 | } 122 | 123 | /** 124 | * https://docs.blockfrost.io/#tag/Cardano-Assets/paths/~1assets~1%7Basset%7D~1transactions/get 125 | */ 126 | public async assetAddresses(asset: Asset): Promise { 127 | return this.sendPaginatedRequest(`/assets/${asset.identifier()}/addresses`) 128 | .then((results: any) => { 129 | return results.map((result: any) => { 130 | return { 131 | address: result.address, 132 | quantity: BigInt(result.quantity), 133 | } as AssetAddress; 134 | }) as AssetAddress[]; 135 | }); 136 | } 137 | 138 | /** 139 | * https://docs.blockfrost.io/#tag/Cardano-Scripts/paths/~1scripts~1datum~1%7Bdatum_hash%7D/get 140 | */ 141 | public async datumValue(datumHash: string): Promise { 142 | return this._limiter.schedule(() => this._api.get(`/scripts/datum/${datumHash}`)) 143 | .then((response: any) => { 144 | return response.data.json_value as DefinitionField; 145 | }); 146 | } 147 | 148 | /** 149 | * https://docs.blockfrost.io/#section/Concepts 150 | */ 151 | private sendPaginatedRequest(url: string, page: number = 1, results: any = []): Promise { 152 | return this._limiter.schedule(() => 153 | this._api.get(url, { 154 | params: { 155 | page, 156 | }, 157 | }) 158 | ).then((response: any) => { 159 | results = results.concat(response.data); 160 | page++; 161 | 162 | // Possibly more data to grab 163 | if (response.data.length === 100) { 164 | return this.sendPaginatedRequest(url, page, results); 165 | } 166 | 167 | return results; 168 | }); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/providers/data/kupo-provider.ts: -------------------------------------------------------------------------------- 1 | import { BaseDataProvider } from './base-data-provider'; 2 | import { Asset } from '@dex/models/asset'; 3 | import { 4 | AssetAddress, 5 | AssetBalance, 6 | DefinitionBytes, 7 | DefinitionConstr, 8 | DefinitionField, 9 | DefinitionInt, 10 | KupoConfig, 11 | RequestConfig, 12 | Transaction, 13 | UTxO 14 | } from '@app/types'; 15 | import axios, { AxiosInstance } from 'axios'; 16 | import { Data } from 'lucid-cardano'; 17 | import { appendSlash } from '@app/utils'; 18 | 19 | export class KupoProvider extends BaseDataProvider { 20 | 21 | private _config: KupoConfig; 22 | private _kupoApi: AxiosInstance; 23 | private _requestConfig: RequestConfig; 24 | 25 | constructor(config: KupoConfig, requestConfig: RequestConfig = {}) { 26 | super(); 27 | 28 | this._requestConfig = Object.assign( 29 | {}, 30 | { 31 | timeout: 5000, 32 | proxyUrl: '', 33 | } as RequestConfig, 34 | requestConfig, 35 | ); 36 | 37 | this._config = config; 38 | this._kupoApi = axios.create({ 39 | baseURL: appendSlash(requestConfig.proxyUrl) + config.url, 40 | timeout: this._requestConfig.timeout, 41 | headers: { 42 | 'Content-Type': 'application/json', 43 | }, 44 | }); 45 | } 46 | 47 | public async utxos(address: string, asset?: Asset): Promise { 48 | const url: string = asset 49 | ? `/matches/${address}?policy_id=${asset.policyId}&asset_name=${asset.nameHex}&unspent` 50 | : `/matches/${address}?unspent`; 51 | 52 | return this._kupoApi.get(url) 53 | .then((results: any) => { 54 | return this.toUtxos(results.data); 55 | }); 56 | } 57 | 58 | public async transactionUtxos(txHash: string): Promise { 59 | return this._kupoApi.get(`/matches/*@${txHash}`) 60 | .then((results: any) => { 61 | return this.toUtxos(results.data); 62 | }); 63 | } 64 | 65 | public async datumValue(datumHash: string): Promise { 66 | return this._kupoApi.get(`/datums/${datumHash}`) 67 | .then((result: any) => { 68 | if (! result.data.datum) { 69 | throw new Error('Datum hash not found.'); 70 | } 71 | 72 | return this.toDefinitionDatum(Data.from(result.data.datum)); 73 | }); 74 | } 75 | 76 | public async assetTransactions(asset: Asset): Promise { 77 | return this._kupoApi.get(`/matches/${asset.identifier('.')}`) 78 | .then((results: any) => { 79 | return results.data.map((result: any) => { 80 | return { 81 | hash: result.transaction_id, 82 | } as Transaction 83 | }) as Transaction[]; 84 | }); 85 | } 86 | 87 | public async assetAddresses(asset: Asset): Promise { 88 | return this._kupoApi.get(`/matches/${asset.identifier('.')}?unspent`) 89 | .then((results: any) => { 90 | return results.data.map((result: any) => { 91 | return { 92 | address: result.address, 93 | quantity: BigInt(result.value.assets[asset.identifier('.')]), 94 | } as AssetAddress 95 | }) as AssetAddress[]; 96 | }); 97 | } 98 | 99 | private toUtxos(results: any): UTxO[] { 100 | return results.map((utxo: any) => { 101 | return { 102 | txHash: utxo.transaction_id, 103 | address: utxo.address, 104 | datumHash: utxo.datum_hash, 105 | outputIndex: utxo.output_index, 106 | assetBalances: (() => { 107 | const balances: AssetBalance[] = [ 108 | { 109 | asset: 'lovelace', 110 | quantity: BigInt(utxo.value.coins), 111 | } 112 | ]; 113 | Object.keys(utxo.value.assets).forEach((unit: string) => { 114 | balances.push({ 115 | asset: Asset.fromIdentifier(unit), 116 | quantity: BigInt(utxo.value.assets[unit]), 117 | }); 118 | }); 119 | return balances; 120 | })(), 121 | } as UTxO; 122 | }) as UTxO[]; 123 | } 124 | 125 | private toDefinitionDatum(unconstructedField: any): DefinitionField { 126 | if (unconstructedField?.fields) { 127 | return { 128 | constructor: unconstructedField.index, 129 | fields: unconstructedField.fields.map((field: any) => this.toDefinitionDatum(field)), 130 | } as DefinitionConstr; 131 | } 132 | 133 | if (typeof unconstructedField === 'bigint') { 134 | return { 135 | int: Number(unconstructedField) 136 | } as DefinitionInt; 137 | } 138 | 139 | if (typeof unconstructedField === 'string') { 140 | return { 141 | bytes: unconstructedField 142 | } as DefinitionBytes; 143 | } 144 | 145 | return unconstructedField; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/providers/data/mock-data-provider.ts: -------------------------------------------------------------------------------- 1 | import { BaseDataProvider } from './base-data-provider'; 2 | import { AssetAddress, DefinitionField, Transaction, UTxO } from '@app/types'; 3 | import { Asset } from '@dex/models/asset'; 4 | 5 | export class MockDataProvider extends BaseDataProvider { 6 | 7 | public async utxos(address: string, asset?: Asset): Promise { 8 | return Promise.resolve([]); 9 | } 10 | 11 | public async assetTransactions(asset: Asset): Promise { 12 | return Promise.resolve([]); 13 | } 14 | 15 | public async transactionUtxos(txHash: string): Promise { 16 | return Promise.resolve([]); 17 | } 18 | 19 | public async assetAddresses(asset: Asset): Promise { 20 | return Promise.resolve([]); 21 | } 22 | 23 | public datumValue(datumHash: string): Promise { 24 | return Promise.resolve({ 25 | constructor: 0, 26 | fields: [], 27 | }); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/providers/wallet/base-wallet-provider.ts: -------------------------------------------------------------------------------- 1 | import { Cip30Api, PayToAddress, WalletOptions } from '@app/types'; 2 | import { DexTransaction } from '@dex/models/dex-transaction'; 3 | 4 | export abstract class BaseWalletProvider { 5 | 6 | public abstract isWalletLoaded: boolean; 7 | 8 | abstract address(): string; 9 | 10 | abstract publicKeyHash(): string; 11 | 12 | abstract stakingKeyHash(): string; 13 | 14 | abstract loadWallet(walletApi: Cip30Api, config: any): Promise; 15 | 16 | abstract loadWalletFromSeedPhrase(seed: string[], options: WalletOptions, config: any): Promise; 17 | 18 | abstract createTransaction(): DexTransaction; 19 | 20 | abstract attachMetadata(transaction: DexTransaction, key: number, json: Object): DexTransaction; 21 | 22 | abstract paymentsForTransaction(transaction: DexTransaction, payToAddresses: PayToAddress[]): Promise; 23 | 24 | abstract signTransaction(transaction: DexTransaction): Promise; 25 | 26 | abstract submitTransaction(transaction: DexTransaction): Promise; 27 | 28 | } -------------------------------------------------------------------------------- /src/providers/wallet/lucid-provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AssetBalance, 3 | BlockfrostConfig, 4 | Cip30Api, 5 | KupmiosConfig, 6 | PayToAddress, 7 | SpendUTxO, 8 | WalletOptions 9 | } from '@app/types'; 10 | import { DexTransaction } from '@dex/models/dex-transaction'; 11 | import { BaseWalletProvider } from './base-wallet-provider'; 12 | import { 13 | Address, 14 | AddressDetails, 15 | Assets, 16 | Blockfrost, 17 | Datum, Kupmios, 18 | Lucid, 19 | TxComplete, 20 | TxHash, 21 | TxSigned, 22 | Unit 23 | } from 'lucid-cardano'; 24 | import { AddressType } from '@app/constants'; 25 | 26 | export class LucidProvider extends BaseWalletProvider { 27 | 28 | public isWalletLoaded: boolean = false; 29 | 30 | private _api: Lucid; 31 | private _usableAddress: string; 32 | private _paymentCredential: string; 33 | private _stakingCredential: string | undefined; 34 | 35 | public address(): string { 36 | return this._usableAddress; 37 | } 38 | 39 | public publicKeyHash(): string { 40 | return this._paymentCredential; 41 | } 42 | 43 | public stakingKeyHash(): string { 44 | return this._stakingCredential ?? ''; 45 | } 46 | 47 | public loadWallet(walletApi: Cip30Api, config: BlockfrostConfig | KupmiosConfig): Promise { 48 | return this.loadLucid(config) 49 | .then((lucid: Lucid) => { 50 | this._api = lucid; 51 | 52 | this._api.selectWallet(walletApi); 53 | 54 | return this.loadWalletInformation(); 55 | }); 56 | } 57 | 58 | public loadWalletFromSeedPhrase(seed: string[], options: WalletOptions = {}, config: BlockfrostConfig | KupmiosConfig): Promise { 59 | return this.loadLucid(config) 60 | .then((lucid: Lucid) => { 61 | this._api = lucid; 62 | 63 | const addressType: 'Base' | 'Enterprise' = options.addressType === AddressType.Enterprise 64 | ? 'Enterprise' 65 | : 'Base'; 66 | 67 | this._api.selectWalletFromSeed( 68 | seed.join(' '), 69 | { 70 | addressType: addressType, 71 | accountIndex: options.accountIndex ?? 0, 72 | }, 73 | ); 74 | 75 | return this.loadWalletInformation(); 76 | }); 77 | } 78 | 79 | public createTransaction(): DexTransaction { 80 | const transaction: DexTransaction = new DexTransaction(this); 81 | transaction.providerData.tx = this._api.newTx(); 82 | 83 | return transaction; 84 | } 85 | 86 | public attachMetadata(transaction: DexTransaction, key: number, json: Object): DexTransaction { 87 | if (! transaction.providerData.tx) { 88 | return transaction; 89 | } 90 | 91 | transaction.providerData.tx.attachMetadata(key, json); 92 | 93 | return transaction; 94 | } 95 | 96 | public async paymentsForTransaction(transaction: DexTransaction, payToAddresses: PayToAddress[]): Promise { 97 | payToAddresses.forEach((payToAddress: PayToAddress) => { 98 | const payment: Assets = this.paymentFromAssets(payToAddress.assetBalances); 99 | 100 | // Include UTxOs to spend 101 | if (payToAddress.spendUtxos && payToAddress.spendUtxos.length > 0) { 102 | payToAddress.spendUtxos.forEach((spendUtxo: SpendUTxO) => { 103 | transaction.providerData.tx.collectFrom([ 104 | { 105 | txHash: spendUtxo.utxo.txHash, 106 | outputIndex: spendUtxo.utxo.outputIndex, 107 | address: spendUtxo.utxo.address, 108 | datumHash: spendUtxo.utxo.datum ? null : spendUtxo.utxo.datumHash, 109 | datum: spendUtxo.utxo.datum, 110 | assets: this.paymentFromAssets(spendUtxo.utxo.assetBalances), 111 | } 112 | ], spendUtxo.redeemer); 113 | 114 | if (spendUtxo.validator) { 115 | transaction.providerData.tx.attachSpendingValidator(spendUtxo.validator); 116 | } 117 | 118 | if (spendUtxo.signer) { 119 | transaction.providerData.tx.addSigner(spendUtxo.signer); 120 | } 121 | }); 122 | } 123 | 124 | switch (payToAddress.addressType) { 125 | case AddressType.Contract: 126 | transaction.providerData.tx.payToContract( 127 | payToAddress.address, 128 | payToAddress.isInlineDatum 129 | ? { 130 | inline: payToAddress.datum as Datum, 131 | } 132 | : payToAddress.datum as Datum, 133 | payment, 134 | ); 135 | break; 136 | case AddressType.Base: 137 | case AddressType.Enterprise: 138 | transaction.providerData.tx.payToAddress( 139 | payToAddress.address, 140 | payment, 141 | ); 142 | break; 143 | default: 144 | throw new Error('Encountered unknown address type.'); 145 | } 146 | }); 147 | 148 | return transaction.providerData.tx.complete() 149 | .then((tx: TxComplete) => { 150 | transaction.providerData.tx = tx; 151 | 152 | return transaction; 153 | }); 154 | } 155 | 156 | public signTransaction(transaction: DexTransaction): Promise { 157 | if (! this.isWalletLoaded) { 158 | throw new Error('Must load wallet before signing transaction.'); 159 | } 160 | 161 | return transaction.providerData.tx.sign().complete() 162 | .then((signedTx: TxSigned) => { 163 | transaction.providerData.tx = signedTx; 164 | 165 | return transaction; 166 | }); 167 | } 168 | 169 | public submitTransaction(transaction: DexTransaction): Promise { 170 | return transaction.providerData.tx.submit() 171 | .then((txHash: TxHash) => { 172 | return txHash; 173 | }); 174 | } 175 | 176 | private paymentFromAssets(assetBalances: AssetBalance[]): Assets { 177 | return assetBalances 178 | .reduce((payment: Record, assetBalance: AssetBalance) => { 179 | payment[assetBalance.asset === 'lovelace' ? 'lovelace' : assetBalance.asset.identifier()] = assetBalance.quantity; 180 | 181 | return payment; 182 | }, {} as Assets); 183 | } 184 | 185 | private loadWalletInformation(): Promise { 186 | return this._api.wallet.address() 187 | .then((address: Address) => { 188 | const details: AddressDetails = this._api.utils.getAddressDetails( 189 | address, 190 | ); 191 | 192 | this._usableAddress = address; 193 | this._paymentCredential = details.paymentCredential?.hash ?? ''; 194 | this._stakingCredential = details.stakeCredential?.hash ?? ''; 195 | 196 | this.isWalletLoaded = true; 197 | 198 | return this as BaseWalletProvider; 199 | }); 200 | } 201 | 202 | private loadLucid(config: BlockfrostConfig | KupmiosConfig): Promise { 203 | return Lucid.new( 204 | 'kupoUrl' in config 205 | ? new Kupmios( 206 | config.kupoUrl, 207 | config.ogmiosUrl 208 | ) 209 | : new Blockfrost( 210 | config.url, 211 | config.projectId 212 | ) 213 | ); 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/providers/wallet/mock-wallet-provider.ts: -------------------------------------------------------------------------------- 1 | import { Cip30Api, PayToAddress, WalletOptions } from '@app/types'; 2 | import { DexTransaction } from '@dex/models/dex-transaction'; 3 | import { BaseWalletProvider } from './base-wallet-provider'; 4 | 5 | export class MockWalletProvider extends BaseWalletProvider { 6 | 7 | public isWalletLoaded: boolean = false; 8 | 9 | private _usableAddress: string; 10 | private _paymentCredential: string; 11 | private _stakingCredential: string; 12 | 13 | constructor() { 14 | super(); 15 | 16 | this._usableAddress = 'addr1test'; 17 | this._paymentCredential = 'ed56'; 18 | this._stakingCredential = 'bac6'; 19 | } 20 | 21 | public address(): string { 22 | return this._usableAddress; 23 | } 24 | 25 | public publicKeyHash(): string { 26 | return this._paymentCredential; 27 | } 28 | 29 | public stakingKeyHash(): string { 30 | return this._stakingCredential; 31 | } 32 | 33 | public loadWallet(walletApi: Cip30Api): Promise { 34 | this.isWalletLoaded = true; 35 | 36 | return Promise.resolve(this as BaseWalletProvider); 37 | } 38 | 39 | public loadWalletFromSeedPhrase(seed: string[], options: WalletOptions = {}): Promise { 40 | this.isWalletLoaded = true; 41 | 42 | return Promise.resolve(this as BaseWalletProvider); 43 | } 44 | 45 | public createTransaction(): DexTransaction { 46 | return new DexTransaction(this); 47 | } 48 | 49 | public attachMetadata(transaction: DexTransaction, key: number, json: Object): DexTransaction { 50 | return transaction; 51 | } 52 | 53 | public paymentsForTransaction(transaction: DexTransaction, payToAddresses: PayToAddress[]): Promise { 54 | return Promise.resolve(transaction); 55 | } 56 | 57 | public signTransaction(transaction: DexTransaction): Promise { 58 | return Promise.resolve(transaction); 59 | } 60 | 61 | public submitTransaction(transaction: DexTransaction): Promise { 62 | return Promise.resolve('hashtest'); 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /src/requests/cancel-swap-request.ts: -------------------------------------------------------------------------------- 1 | import { Dexter } from '@app/dexter'; 2 | import { DexTransaction } from '@dex/models/dex-transaction'; 3 | import { PayToAddress, UTxO } from '@app/types'; 4 | import { MetadataKey, TransactionStatus } from '@app/constants'; 5 | import { BaseDataProvider } from '@providers/data/base-data-provider'; 6 | 7 | export class CancelSwapRequest { 8 | 9 | private _dexter: Dexter; 10 | private _txHash: string; 11 | private _dexName: string; 12 | 13 | constructor(dexter: Dexter) { 14 | this._dexter = dexter; 15 | } 16 | 17 | public forTransaction(txHash: string): CancelSwapRequest { 18 | this._txHash = txHash; 19 | 20 | return this; 21 | } 22 | 23 | public forDex(name: string): CancelSwapRequest { 24 | this._dexName = name; 25 | 26 | return this; 27 | } 28 | 29 | public getPaymentsToAddresses(): Promise { 30 | if (! this._dexter.walletProvider) { 31 | throw new Error('Wallet provider must be set before submitting a swap order.'); 32 | } 33 | 34 | const returnAddress: string = this._dexter.walletProvider.address(); 35 | 36 | return (this._dexter.dataProvider as BaseDataProvider).transactionUtxos(this._txHash) 37 | .then((utxos: UTxO[]) => { 38 | return this._dexter.availableDexs[this._dexName].buildCancelSwapOrder(utxos, returnAddress); 39 | }) 40 | .catch(() => { 41 | throw new Error('Unable to grab UTxOs for the provided Tx hash. Ensure the one provided is a valid Tx hash.') 42 | }); 43 | } 44 | 45 | public cancel(): DexTransaction { 46 | if (! this._dexter.walletProvider) { 47 | throw new Error('Wallet provider must be set before submitting a swap order.'); 48 | } 49 | if (! this._txHash) { 50 | throw new Error('Tx hash must be provided before cancelling a swap order.'); 51 | } 52 | if (! this._dexName) { 53 | throw new Error('DEX must be provided before cancelling a swap order.'); 54 | } 55 | 56 | const cancelTransaction: DexTransaction = this._dexter.walletProvider.createTransaction(); 57 | 58 | this.getPaymentsToAddresses() 59 | .then((payToAddresses: PayToAddress[]) => { 60 | this.sendCancelOrder(cancelTransaction, payToAddresses); 61 | }) 62 | .catch((error) => { 63 | throw new Error(`Unable to cancel swap order. ${error}`) 64 | }); 65 | 66 | return cancelTransaction; 67 | } 68 | 69 | private sendCancelOrder(cancelTransaction: DexTransaction, payToAddresses: PayToAddress[]) { 70 | cancelTransaction.status = TransactionStatus.Building; 71 | 72 | cancelTransaction.attachMetadata(MetadataKey.Message, { 73 | msg: [ 74 | `[${this._dexter.config.metadataMsgBranding}] ${this._dexName} Cancel Swap` 75 | ] 76 | }); 77 | 78 | // Build transaction 79 | cancelTransaction.payToAddresses(payToAddresses) 80 | .then(() => { 81 | cancelTransaction.status = TransactionStatus.Signing; 82 | 83 | // Sign transaction 84 | cancelTransaction.sign() 85 | .then(() => { 86 | cancelTransaction.status = TransactionStatus.Submitting; 87 | 88 | // Submit transaction 89 | cancelTransaction.submit() 90 | .then(() => { 91 | cancelTransaction.status = TransactionStatus.Submitted; 92 | }) 93 | .catch((error) => { 94 | cancelTransaction.error = { 95 | step: TransactionStatus.Submitting, 96 | reason: 'Failed submitting transaction.', 97 | reasonRaw: error, 98 | }; 99 | cancelTransaction.status = TransactionStatus.Errored; 100 | }); 101 | }) 102 | .catch((error) => { 103 | cancelTransaction.error = { 104 | step: TransactionStatus.Signing, 105 | reason: 'Failed to sign transaction.', 106 | reasonRaw: error, 107 | }; 108 | cancelTransaction.status = TransactionStatus.Errored; 109 | }); 110 | }) 111 | .catch((error) => { 112 | cancelTransaction.error = { 113 | step: TransactionStatus.Building, 114 | reason: 'Failed to build transaction.', 115 | reasonRaw: error, 116 | }; 117 | cancelTransaction.status = TransactionStatus.Errored; 118 | }); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/requests/split-cancel-swap-request.ts: -------------------------------------------------------------------------------- 1 | import { Dexter } from '@app/dexter'; 2 | import { CancelSwapRequest } from '@requests/cancel-swap-request'; 3 | import { PayToAddress, SplitCancelSwapMapping } from '@app/types'; 4 | import { DexTransaction } from '@dex/models/dex-transaction'; 5 | import { MetadataKey, TransactionStatus } from '@app/constants'; 6 | 7 | export class SplitCancelSwapRequest { 8 | 9 | private _dexter: Dexter; 10 | private _cancelRequests: CancelSwapRequest[] = []; 11 | 12 | constructor(dexter: Dexter) { 13 | this._dexter = dexter; 14 | } 15 | 16 | public forTransactions(mappings: SplitCancelSwapMapping[]): SplitCancelSwapRequest { 17 | this._cancelRequests = mappings.map((mapping: SplitCancelSwapMapping) => { 18 | return this._dexter.newCancelSwapRequest() 19 | .forTransaction(mapping.txHash) 20 | .forDex(mapping.dex); 21 | }); 22 | 23 | return this; 24 | } 25 | 26 | public submit(): DexTransaction { 27 | if (! this._dexter.walletProvider) { 28 | throw new Error('Wallet provider must be set before submitting a cancel swap order.'); 29 | } 30 | if (! this._dexter.walletProvider.isWalletLoaded) { 31 | throw new Error('Wallet must be loaded before submitting a cancel swap order.'); 32 | } 33 | if (this._cancelRequests.length === 0) { 34 | throw new Error('Cancel requests were never initialized.'); 35 | } 36 | 37 | const cancelTransaction: DexTransaction = this._dexter.walletProvider.createTransaction(); 38 | 39 | Promise.all(this._cancelRequests.map((cancelRequest: CancelSwapRequest) => cancelRequest.getPaymentsToAddresses())) 40 | .then((payToAddresses: PayToAddress[][]) => { 41 | this.sendSplitCancelSwapOrder(cancelTransaction, payToAddresses.flat()); 42 | }); 43 | 44 | return cancelTransaction; 45 | } 46 | 47 | private sendSplitCancelSwapOrder(cancelTransaction: DexTransaction, payToAddresses: PayToAddress[]) { 48 | cancelTransaction.status = TransactionStatus.Building; 49 | 50 | cancelTransaction.attachMetadata(MetadataKey.Message, { 51 | msg: [ 52 | `[${this._dexter.config.metadataMsgBranding}] Split Cancel Swap` 53 | ] 54 | }); 55 | 56 | // Build transaction 57 | cancelTransaction.payToAddresses(payToAddresses) 58 | .then(() => { 59 | cancelTransaction.status = TransactionStatus.Signing; 60 | 61 | // Sign transaction 62 | cancelTransaction.sign() 63 | .then(() => { 64 | cancelTransaction.status = TransactionStatus.Submitting; 65 | 66 | // Submit transaction 67 | cancelTransaction.submit() 68 | .then(() => { 69 | cancelTransaction.status = TransactionStatus.Submitted; 70 | }) 71 | .catch((error) => { 72 | cancelTransaction.error = { 73 | step: TransactionStatus.Submitting, 74 | reason: 'Failed submitting transaction.', 75 | reasonRaw: error, 76 | }; 77 | cancelTransaction.status = TransactionStatus.Errored; 78 | }); 79 | }) 80 | .catch((error) => { 81 | cancelTransaction.error = { 82 | step: TransactionStatus.Signing, 83 | reason: 'Failed to sign transaction.', 84 | reasonRaw: error, 85 | }; 86 | cancelTransaction.status = TransactionStatus.Errored; 87 | }); 88 | }) 89 | .catch((error) => { 90 | cancelTransaction.error = { 91 | step: TransactionStatus.Building, 92 | reason: 'Failed to build transaction.', 93 | reasonRaw: error, 94 | }; 95 | cancelTransaction.status = TransactionStatus.Errored; 96 | }); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { AddressType, DatumParameterKey, TransactionStatus } from './constants'; 2 | import { Token } from '@dex/models/asset'; 3 | import { BaseDex } from '@dex/base-dex'; 4 | import { LiquidityPool } from '@dex/models/liquidity-pool'; 5 | import { Script } from 'lucid-cardano'; 6 | 7 | export interface DexterConfig { 8 | shouldFetchMetadata?: boolean; 9 | shouldFallbackToApi?: boolean; 10 | shouldSubmitOrders?: boolean; 11 | metadataMsgBranding?: string; 12 | } 13 | 14 | export interface RequestConfig { 15 | timeout?: number; 16 | proxyUrl?: string; 17 | retries?: number; 18 | } 19 | 20 | export interface BlockfrostConfig { 21 | url: string; 22 | projectId: string; 23 | } 24 | 25 | export interface KupoConfig { 26 | url: string; 27 | } 28 | 29 | export interface KupmiosConfig { 30 | kupoUrl: string; 31 | ogmiosUrl: string; 32 | } 33 | 34 | export type AvailableDexs = { 35 | [dex: string]: BaseDex; 36 | }; 37 | 38 | export type DatumParameters = { 39 | [key in DatumParameterKey | string]?: string | number | bigint; 40 | }; 41 | 42 | export type AssetBalance = { 43 | asset: Token; 44 | quantity: bigint; 45 | }; 46 | 47 | export type UTxO = { 48 | txHash: string; 49 | address: string; 50 | datumHash: string; 51 | datum?: string; 52 | outputIndex: number; 53 | assetBalances: AssetBalance[]; 54 | }; 55 | 56 | export type Transaction = { 57 | hash: string; 58 | inputs: UTxO[]; 59 | outputs: UTxO[]; 60 | }; 61 | 62 | export type AssetAddress = { 63 | address: string; 64 | quantity: bigint; 65 | }; 66 | 67 | export type DefinitionBytes = { 68 | bytes: string | DatumParameterKey; 69 | }; 70 | 71 | export type DefinitionInt = { 72 | int: number | DatumParameterKey; 73 | }; 74 | 75 | export type DefinitionList = { 76 | list: DefinitionField[] | DefinitionList[]; 77 | }; 78 | 79 | export type DefinitionField = DefinitionConstr | DefinitionBytes | DefinitionInt | DefinitionList| Function | DefinitionField[]; 80 | 81 | export type DefinitionConstr = { 82 | constructor: number | DatumParameterKey; 83 | fields: DefinitionField[]; 84 | }; 85 | 86 | export type WalletOptions = { 87 | addressType?: AddressType; 88 | accountIndex?: number; 89 | }; 90 | 91 | export type SpendUTxO = { 92 | utxo: UTxO; 93 | redeemer?: string; 94 | validator?: Script; 95 | signer?: string; 96 | }; 97 | 98 | export type PayToAddress = { 99 | address: string; 100 | addressType: AddressType; 101 | assetBalances: AssetBalance[]; 102 | spendUtxos?: SpendUTxO[]; 103 | datum?: string; 104 | isInlineDatum: boolean; 105 | }; 106 | 107 | export type SwapFee = { 108 | id: string; 109 | title: string; 110 | description: string; 111 | value: bigint; 112 | isReturned: boolean; 113 | }; 114 | 115 | export type SwapInAmountMapping = { 116 | swapInAmount: bigint; 117 | liquidityPool: LiquidityPool; 118 | }; 119 | 120 | export type SwapOutAmountMapping = { 121 | swapOutAmount: bigint; 122 | liquidityPool: LiquidityPool; 123 | }; 124 | 125 | export type SplitCancelSwapMapping = { 126 | txHash: string; 127 | dex: string; 128 | }; 129 | 130 | export type DexTransactionError = { 131 | step: TransactionStatus; 132 | reason: string; 133 | reasonRaw: string; 134 | }; 135 | 136 | export type AssetMetadata = { 137 | policyId: string; 138 | nameHex: string; 139 | decimals: number; 140 | }; 141 | 142 | export type Cip30Api = { 143 | getNetworkId(): Promise; 144 | getUtxos(): Promise; 145 | getBalance(): Promise; 146 | getUsedAddresses(): Promise; 147 | getUnusedAddresses(): Promise; 148 | getChangeAddress(): Promise; 149 | getRewardAddresses(): Promise; 150 | signTx(tx: string, partialSign: boolean): Promise; 151 | signData( 152 | address: string, 153 | payload: string 154 | ): Promise<{ 155 | signature: string; 156 | key: string; 157 | }>; 158 | submitTx(tx: string): Promise; 159 | getCollateral(): Promise; 160 | experimental: { 161 | getCollateral(): Promise; 162 | on(eventName: string, callback: (...args: unknown[]) => void): void; 163 | off(eventName: string, callback: (...args: unknown[]) => void): void; 164 | }; 165 | }; 166 | 167 | export type DatumJson = { 168 | int?: number; 169 | bytes?: string; 170 | list?: Array; 171 | map?: Array<{ k: unknown; v: unknown }>; 172 | fields?: Array; 173 | [constructor: string]: unknown; 174 | }; -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '@dex/models/asset'; 2 | import { LiquidityPool } from '@dex/models/liquidity-pool'; 3 | import { C, Datum, fromHex, Lucid, toHex, Utils } from 'lucid-cardano'; 4 | import { DatumJson } from '@app/types'; 5 | import { encoder } from 'js-encoding-utils'; 6 | 7 | export const lucidUtils: Utils = new Utils(new Lucid()); 8 | 9 | export function tokensMatch(tokenA: Token, tokenB: Token): boolean { 10 | const tokenAId: string = tokenA === 'lovelace' ? 'lovelace' : tokenA.identifier(); 11 | const tokenBId: string = tokenB === 'lovelace' ? 'lovelace' : tokenB.identifier(); 12 | 13 | return tokenAId === tokenBId; 14 | } 15 | 16 | export function correspondingReserves(liquidityPool: LiquidityPool, token: Token): bigint[] { 17 | return tokensMatch(token, liquidityPool.assetA) 18 | ? [liquidityPool.reserveA, liquidityPool.reserveB] 19 | : [liquidityPool.reserveB, liquidityPool.reserveA] 20 | } 21 | 22 | export function appendSlash(value?: string) { 23 | if (! value) return ''; 24 | if (value.endsWith('/')) return; 25 | 26 | return `${value}/`; 27 | } 28 | 29 | /** 30 | * Modified version from lucid 31 | */ 32 | export function datumJsonToCbor(json: DatumJson): Datum { 33 | const convert = (json: DatumJson): C.PlutusData => { 34 | if (!isNaN(json.int!)) { 35 | return C.PlutusData.new_integer(C.BigInt.from_str(json.int!.toString())); 36 | } else if (json.bytes || !isNaN(Number(json.bytes))) { 37 | return C.PlutusData.new_bytes(fromHex(json.bytes!)); 38 | } else if (json.map) { 39 | const l = C.PlutusList.new(); 40 | (json as any).forEach((v: DatumJson) => { 41 | l.add(convert(v)); 42 | }); 43 | return C.PlutusData.new_list(l); 44 | } else if (json.list) { 45 | const l = C.PlutusList.new(); 46 | json.list.forEach((v: DatumJson) => { 47 | l.add(convert(v)); 48 | }); 49 | return C.PlutusData.new_list(l); 50 | } else if (!isNaN(json.constructor! as unknown as number)) { 51 | const l = C.PlutusList.new(); 52 | json.fields!.forEach((v: DatumJson) => { 53 | l.add(convert(v)); 54 | }); 55 | return C.PlutusData.new_constr_plutus_data( 56 | C.ConstrPlutusData.new( 57 | C.BigNum.from_str(json.constructor!.toString()), 58 | l, 59 | ), 60 | ); 61 | } 62 | throw new Error("Unsupported type"); 63 | }; 64 | 65 | return toHex(convert(json).to_bytes()); 66 | } 67 | 68 | export const bytesToHex = (bytes: Uint8Array): string => encoder.arrayBufferToHexString(bytes); 69 | export const hexToBytes = (hex: string): Uint8Array => encoder.hexStringToArrayBuffer(hex); -------------------------------------------------------------------------------- /tests/dex-transaction.test.ts: -------------------------------------------------------------------------------- 1 | import { DexTransaction, MockWalletProvider, TransactionStatus } from '../src'; 2 | 3 | describe('DexTransaction', () => { 4 | 5 | const transaction: DexTransaction = new DexTransaction(new MockWalletProvider()); 6 | 7 | it('Fails to submit when un-signed', () => { 8 | expect(() => { transaction.submit(); }).toThrowError(); 9 | }); 10 | 11 | it('Can sign', () => { 12 | return transaction.sign() 13 | .then(() => { 14 | expect(transaction.isSigned).toBe(true); 15 | }); 16 | }); 17 | 18 | it('Can submit', () => { 19 | return transaction.submit() 20 | .then(() => { 21 | expect(transaction.hash).toBe('hashtest'); 22 | }); 23 | }); 24 | 25 | it('Can expose building status', () => { 26 | const testTransaction: DexTransaction = new DexTransaction(new MockWalletProvider()); 27 | let receivedStatus: boolean = false; 28 | 29 | testTransaction.onBuilding(() => receivedStatus = true); 30 | 31 | testTransaction.status = TransactionStatus.Building; 32 | 33 | expect(receivedStatus).toBe(true); 34 | }); 35 | 36 | it('Can expose signing status', () => { 37 | const testTransaction: DexTransaction = new DexTransaction(new MockWalletProvider()); 38 | let receivedStatus: boolean = false; 39 | 40 | testTransaction.onSigning(() => receivedStatus = true); 41 | 42 | testTransaction.status = TransactionStatus.Signing; 43 | 44 | expect(receivedStatus).toBe(true); 45 | }); 46 | 47 | it('Can expose submitting status', () => { 48 | const testTransaction: DexTransaction = new DexTransaction(new MockWalletProvider()); 49 | let receivedStatus: boolean = false; 50 | 51 | testTransaction.onSubmitting(() => receivedStatus = true); 52 | 53 | testTransaction.status = TransactionStatus.Submitting; 54 | 55 | expect(receivedStatus).toBe(true); 56 | }); 57 | 58 | it('Can expose submitted status', () => { 59 | const testTransaction: DexTransaction = new DexTransaction(new MockWalletProvider()); 60 | let receivedStatus: boolean = false; 61 | 62 | testTransaction.onSubmitted(() => receivedStatus = true); 63 | 64 | testTransaction.status = TransactionStatus.Submitted; 65 | 66 | expect(receivedStatus).toBe(true); 67 | }); 68 | 69 | it('Can expose error status', () => { 70 | const testTransaction: DexTransaction = new DexTransaction(new MockWalletProvider()); 71 | let receivedStatus: boolean = false; 72 | 73 | testTransaction.onError(() => receivedStatus = true); 74 | 75 | testTransaction.status = TransactionStatus.Errored; 76 | 77 | expect(receivedStatus).toBe(true); 78 | }); 79 | 80 | }); 81 | -------------------------------------------------------------------------------- /tests/minswap.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Asset, 3 | Dexter, 4 | LiquidityPool, 5 | Minswap, 6 | MockDataProvider, 7 | SwapRequest, 8 | MockWalletProvider, 9 | DatumParameters, 10 | DatumParameterKey, 11 | PayToAddress, 12 | AddressType, 13 | UTxO 14 | } from '../src'; 15 | 16 | describe('Minswap', () => { 17 | let minswap: Minswap; 18 | const returnAddress = 'mockBlockchainAddress123'; 19 | 20 | beforeEach(() => { 21 | minswap = new Minswap(); 22 | }); 23 | const walletProvider: MockWalletProvider = new MockWalletProvider(); 24 | walletProvider.loadWalletFromSeedPhrase(['']); 25 | const dexter: Dexter = (new Dexter()) 26 | .withDataProvider(new MockDataProvider()) 27 | .withWalletProvider(walletProvider); 28 | const asset: Asset = new Asset('f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880', '69555344', 6); 29 | 30 | 31 | describe('Set Swap In', () => { 32 | 33 | const liquidityPool: LiquidityPool = new LiquidityPool( 34 | Minswap.identifier, 35 | 'lovelace', 36 | asset, 37 | 30817255371488n, 38 | 349805856622734n, 39 | 'addr1', 40 | ); 41 | liquidityPool.poolFeePercent = 0.3; 42 | 43 | const swapRequest: SwapRequest = dexter.newSwapRequest() 44 | .forLiquidityPool(liquidityPool) 45 | .withSwapInToken('lovelace') 46 | .withSwapInAmount(10_000_000_000000n) 47 | .withSlippagePercent(0.5); 48 | 49 | it('Can calculate swap parameters', () => { 50 | expect(+swapRequest.getPriceImpactPercent().toFixed(2)).toEqual(24.37); 51 | expect(swapRequest.getEstimatedReceive()).toEqual(85_506_228_814959n); 52 | expect(swapRequest.getMinimumReceive()).toEqual(85_080_824_691501n); 53 | }); 54 | 55 | it('Can build swap order', () => { 56 | const minswap: Minswap = new Minswap(); 57 | const defaultSwapParameters: DatumParameters = { 58 | [DatumParameterKey.SenderPubKeyHash]: walletProvider.publicKeyHash(), 59 | [DatumParameterKey.SenderStakingKeyHash]: walletProvider.stakingKeyHash(), 60 | [DatumParameterKey.ReceiverPubKeyHash]: walletProvider.publicKeyHash(), 61 | [DatumParameterKey.ReceiverStakingKeyHash]: walletProvider.stakingKeyHash(), 62 | [DatumParameterKey.SwapInAmount]: swapRequest.swapInAmount, 63 | [DatumParameterKey.MinReceive]: swapRequest.getMinimumReceive(), 64 | [DatumParameterKey.SwapInTokenPolicyId]: '', 65 | [DatumParameterKey.SwapInTokenAssetName]: '', 66 | [DatumParameterKey.SwapOutTokenPolicyId]: asset.policyId, 67 | [DatumParameterKey.SwapOutTokenAssetName]: asset.nameHex, 68 | }; 69 | 70 | return minswap.buildSwapOrder(liquidityPool, defaultSwapParameters) 71 | .then((payments: PayToAddress[]) => { 72 | expect(() => { minswap.buildSwapOrder(liquidityPool, defaultSwapParameters); }).not.toThrowError(); 73 | expect(payments[0].addressType).toBe(AddressType.Contract); 74 | expect(payments[0].assetBalances[0].quantity).toEqual(10000004000000n); 75 | expect(payments[0].datum).toBe('d8799fd8799fd8799f42ed56ffd8799fd8799fd8799f42ed56ffffffffd8799fd8799f42ed56ffd8799fd8799fd8799f42bac6ffffffffd87a80d8799fd8799f581cf66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b698804469555344ff1b00004d616c553b2dff1a001e84801a001e8480ff'); 76 | }); 77 | }); 78 | 79 | }); 80 | 81 | describe('Set Swap Out', () => { 82 | const liquidityPool: LiquidityPool = new LiquidityPool( 83 | Minswap.identifier, 84 | 'lovelace', 85 | asset, 86 | 5126788000507n, 87 | 1405674367646n, 88 | 'addr1', 89 | ); 90 | liquidityPool.poolFeePercent = 0.3; 91 | 92 | const swapRequest: SwapRequest = dexter.newSwapRequest() 93 | .forLiquidityPool(liquidityPool) 94 | .withSwapInToken('lovelace') 95 | .withSwapOutAmount(100_000000n) 96 | .withSlippagePercent(0.5); 97 | 98 | it('Can calculate swap parameters', () => { 99 | expect(swapRequest.swapInAmount).toEqual(365844367n); 100 | }); 101 | 102 | }); 103 | 104 | describe('Minswap Cancel Order', () => { 105 | 106 | it('should successfully cancel an order', async () => { 107 | let marketOrderAddress = minswap.marketOrderAddress; 108 | const txOutputs: UTxO[] = [ 109 | { 110 | txHash: 'mockTxHash123', 111 | address: marketOrderAddress, 112 | datumHash: 'mockDatumHash123', 113 | outputIndex: 0, 114 | assetBalances: [{ asset: 'lovelace', quantity: 1000000000000n }] 115 | } 116 | ]; 117 | 118 | const result = await minswap.buildCancelSwapOrder(txOutputs, returnAddress); 119 | 120 | expect(result).toBeDefined(); 121 | expect(result[0].address).toBe(returnAddress); 122 | }); 123 | 124 | it('should fail to cancel an order with invalid UTxO', async () => { 125 | const invalidTxOutputs: UTxO[] = [ 126 | { 127 | txHash: 'invalidTxHash', 128 | address: 'invalidAddress', 129 | datumHash: 'invalidDatumHash', 130 | outputIndex: 0, 131 | assetBalances: [{ asset: 'lovelace', quantity: 1000n }] 132 | } 133 | ]; 134 | 135 | 136 | try { 137 | await minswap.buildCancelSwapOrder(invalidTxOutputs, returnAddress); 138 | fail('Expected buildCancelSwapOrder to throw an error'); 139 | } catch (error: unknown) { 140 | if (error instanceof Error) { 141 | expect(error.message).toContain('Unable to find relevant UTxO for cancelling the swap order.'); 142 | } 143 | } 144 | }); 145 | }); 146 | 147 | }); 148 | -------------------------------------------------------------------------------- /tests/muesliswap.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Asset, 3 | Dexter, 4 | LiquidityPool, 5 | MuesliSwap, 6 | MockDataProvider, 7 | SwapRequest, 8 | MockWalletProvider, 9 | DatumParameters, 10 | DatumParameterKey, 11 | PayToAddress, 12 | AddressType, 13 | UTxO 14 | } from '../src'; 15 | 16 | describe('MuesliSwap', () => { 17 | 18 | const walletProvider: MockWalletProvider = new MockWalletProvider(); 19 | walletProvider.loadWalletFromSeedPhrase(['']); 20 | const dexter: Dexter = (new Dexter()) 21 | .withDataProvider(new MockDataProvider()) 22 | .withWalletProvider(walletProvider); 23 | const asset: Asset = new Asset('f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880', '69555344', 6); 24 | 25 | describe('Set Swap In', () => { 26 | 27 | const liquidityPool: LiquidityPool = new LiquidityPool( 28 | MuesliSwap.identifier, 29 | 'lovelace', 30 | asset, 31 | 1386837721743n, 32 | 485925n, 33 | 'addr1', 34 | ); 35 | liquidityPool.poolFeePercent = 0.3; 36 | 37 | const swapRequest: SwapRequest = dexter.newSwapRequest() 38 | .forLiquidityPool(liquidityPool) 39 | .withSwapInToken('lovelace') 40 | .withSwapInAmount(100_000_000000n) 41 | .withSlippagePercent(3.0); 42 | 43 | it('Can calculate swap parameters', () => { 44 | expect(+swapRequest.getPriceImpactPercent().toFixed(2)).toEqual(7.51); 45 | expect(swapRequest.getEstimatedReceive()).toEqual(32590n); 46 | expect(swapRequest.getMinimumReceive()).toEqual(31640n); 47 | }); 48 | 49 | it('Can build swap order', () => { 50 | const muesliswap: MuesliSwap = new MuesliSwap(); 51 | const defaultSwapParameters: DatumParameters = { 52 | [DatumParameterKey.SenderPubKeyHash]: walletProvider.publicKeyHash(), 53 | [DatumParameterKey.SenderStakingKeyHash]: walletProvider.stakingKeyHash(), 54 | [DatumParameterKey.ReceiverPubKeyHash]: walletProvider.publicKeyHash(), 55 | [DatumParameterKey.ReceiverStakingKeyHash]: walletProvider.stakingKeyHash(), 56 | [DatumParameterKey.SwapInAmount]: swapRequest.swapInAmount, 57 | [DatumParameterKey.MinReceive]: swapRequest.getMinimumReceive(), 58 | [DatumParameterKey.SwapInTokenPolicyId]: '', 59 | [DatumParameterKey.SwapInTokenAssetName]: '', 60 | [DatumParameterKey.SwapOutTokenPolicyId]: asset.policyId, 61 | [DatumParameterKey.SwapOutTokenAssetName]: asset.nameHex, 62 | }; 63 | 64 | return muesliswap.buildSwapOrder(liquidityPool, defaultSwapParameters) 65 | .then((payments: PayToAddress[]) => { 66 | expect(() => { muesliswap.buildSwapOrder(liquidityPool, defaultSwapParameters); }).not.toThrowError(); 67 | expect(payments[0].addressType).toBe(AddressType.Contract); 68 | expect(payments[0].assetBalances[0].quantity).toEqual(100002650000n); 69 | expect(payments[0].datum).toBe('d8799fd8799fd8799fd8799f42ed56ffd8799fd8799fd8799f42bac6ffffffff581cf66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b6988044695553444040197b98d87a801a00286f90ffff'); 70 | }); 71 | }); 72 | 73 | }); 74 | 75 | describe('Set Swap Out', () => { 76 | const liquidityPool: LiquidityPool = new LiquidityPool( 77 | MuesliSwap.identifier, 78 | 'lovelace', 79 | asset, 80 | 67011967873n, 81 | 5026969972n, 82 | 'addr1', 83 | ); 84 | liquidityPool.poolFeePercent = 0.3; 85 | 86 | const swapRequest: SwapRequest = dexter.newSwapRequest() 87 | .forLiquidityPool(liquidityPool) 88 | .withSwapInToken('lovelace') 89 | .withSwapOutAmount(10_000000n) 90 | .withSlippagePercent(0.5); 91 | 92 | it('Can calculate swap parameters', () => { 93 | expect(swapRequest.swapInAmount).toEqual(133971309n); 94 | }); 95 | 96 | }); 97 | 98 | describe('Muesliswap Cancel Order', () => { 99 | let muesliswap: MuesliSwap; 100 | const returnAddress = 'mockBlockchainAddress123'; 101 | 102 | beforeEach(() => { 103 | muesliswap = new MuesliSwap(); 104 | }); 105 | 106 | it('should successfully cancel an order', async () => { 107 | let orderAddress = muesliswap.orderAddress; 108 | const txOutputs: UTxO[] = [ 109 | { 110 | txHash: 'mockTxHash123', 111 | address: orderAddress, 112 | datumHash: 'mockDatumHash123', 113 | outputIndex: 0, 114 | assetBalances: [{ asset: 'lovelace', quantity: 10000n }] 115 | } 116 | ]; 117 | 118 | const result = await muesliswap.buildCancelSwapOrder(txOutputs, returnAddress); 119 | 120 | expect(result).toBeDefined(); 121 | expect(result[0].address).toBe(returnAddress); 122 | }); 123 | 124 | it('should fail to cancel an order with invalid UTxO', async () => { 125 | const invalidTxOutputs: UTxO[] = [ 126 | { 127 | txHash: 'invalidTxHash', 128 | address: 'invalidAddress', 129 | datumHash: 'invalidDatumHash', 130 | outputIndex: 0, 131 | assetBalances: [{ asset: 'lovelace', quantity: 10000n }] 132 | } 133 | ]; 134 | 135 | try { 136 | await muesliswap.buildCancelSwapOrder(invalidTxOutputs, returnAddress); 137 | fail('Expected buildCancelSwapOrder to throw an error'); 138 | } catch (error: unknown) { 139 | if (error instanceof Error) { 140 | expect(error.message).toContain('Unable to find relevant UTxO for cancelling the swap order.'); 141 | } 142 | } 143 | }); 144 | 145 | }); 146 | 147 | }); 148 | -------------------------------------------------------------------------------- /tests/split-swap-request.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Asset, 3 | Dexter, 4 | LiquidityPool, 5 | MockDataProvider, 6 | MockWalletProvider, 7 | WingRiders, 8 | Minswap 9 | } from '../src'; 10 | import { SplitSwapRequest } from '../src/requests/split-swap-request'; 11 | 12 | describe('SplitSwapRequest', () => { 13 | 14 | const walletProvider: MockWalletProvider = new MockWalletProvider(); 15 | walletProvider.loadWalletFromSeedPhrase(['']); 16 | const dexter: Dexter = (new Dexter()) 17 | .withDataProvider(new MockDataProvider()) 18 | .withWalletProvider(walletProvider); 19 | const asset: Asset = new Asset('f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880', '69555344', 6); 20 | 21 | const minswapPool: LiquidityPool = new LiquidityPool( 22 | Minswap.identifier, 23 | 'lovelace', 24 | asset, 25 | 30817255371488n, 26 | 349805856622734n, 27 | 'addr1', 28 | ); 29 | minswapPool.poolFeePercent = 0.3 30 | 31 | const wingridersPool: LiquidityPool = new LiquidityPool( 32 | WingRiders.identifier, 33 | 'lovelace', 34 | asset, 35 | 923224398616n, 36 | 7942169n, 37 | 'addr1', 38 | ); 39 | wingridersPool.poolFeePercent = 0.35; 40 | 41 | describe('Parameter setting', () => { 42 | 43 | const swapRequest: SplitSwapRequest = dexter.newSplitSwapRequest(); 44 | 45 | it('Can set swap in token', () => { 46 | swapRequest.withSwapInToken('lovelace') 47 | 48 | expect(swapRequest.swapInToken).toBe('lovelace'); 49 | }); 50 | 51 | it('Can set swap out token', () => { 52 | swapRequest.withSwapOutToken('lovelace') 53 | 54 | expect(swapRequest.swapOutToken).toBe('lovelace'); 55 | }); 56 | 57 | it('Can set swap in mappings', () => { 58 | swapRequest.withSwapInAmountMappings([ 59 | { 60 | swapInAmount: 2_000000n, 61 | liquidityPool: minswapPool, 62 | } 63 | ]) 64 | 65 | expect(swapRequest.swapRequests[0].liquidityPool.uuid).toBe(minswapPool.uuid); 66 | expect(swapRequest.swapRequests[0].swapInAmount).toBe(2_000000n); 67 | }); 68 | 69 | it('Can set swap out mappings', () => { 70 | swapRequest.withSwapOutAmountMappings([ 71 | { 72 | swapOutAmount: 2_000000n, 73 | liquidityPool: minswapPool, 74 | } 75 | ]) 76 | 77 | expect(swapRequest.swapRequests[0].liquidityPool.uuid).toBe(minswapPool.uuid); 78 | }); 79 | 80 | }) 81 | 82 | describe('Alter Order', () => { 83 | 84 | it('Can flip all swap in tokens', () => { 85 | const swapRequest: SplitSwapRequest = dexter.newSplitSwapRequest(); 86 | 87 | swapRequest 88 | .withSwapInToken(asset) 89 | .withSwapInAmountMappings([ 90 | { 91 | swapInAmount: 2_000000n, 92 | liquidityPool: minswapPool, 93 | }, 94 | { 95 | swapInAmount: 2_000000n, 96 | liquidityPool: wingridersPool, 97 | } 98 | ]) 99 | .flip(); 100 | 101 | expect(swapRequest.swapRequests[0].swapInToken).toBe('lovelace'); 102 | expect(swapRequest.swapRequests[1].swapInToken).toBe('lovelace'); 103 | }); 104 | 105 | it('Can calculate avg. price impact percent', () => { 106 | const swapRequest: SplitSwapRequest = dexter.newSplitSwapRequest(); 107 | 108 | swapRequest 109 | .withSwapInToken('lovelace') 110 | .withSlippagePercent(0.5) 111 | .withSwapInAmountMappings([ 112 | { 113 | swapInAmount: 10_000_000_000000n, 114 | liquidityPool: minswapPool, 115 | }, 116 | { 117 | swapInAmount: 10_000_000000n, 118 | liquidityPool: wingridersPool, 119 | } 120 | ]); 121 | 122 | expect(+swapRequest.getAvgPriceImpactPercent().toFixed(2)).toEqual(12.90); 123 | }); 124 | 125 | }); 126 | 127 | }); 128 | -------------------------------------------------------------------------------- /tests/sundaeswap-v1.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Asset, 3 | Dexter, 4 | LiquidityPool, 5 | SundaeSwapV1, 6 | MockDataProvider, 7 | SwapRequest, 8 | MockWalletProvider, 9 | DatumParameters, 10 | DatumParameterKey, 11 | PayToAddress, 12 | AddressType, 13 | UTxO, 14 | } from '../src'; 15 | 16 | describe('SundaeSwapV1', () => { 17 | 18 | const walletProvider: MockWalletProvider = new MockWalletProvider(); 19 | walletProvider.loadWalletFromSeedPhrase(['']); 20 | const dexter: Dexter = (new Dexter()) 21 | .withDataProvider(new MockDataProvider()) 22 | .withWalletProvider(walletProvider); 23 | const asset: Asset = new Asset('f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880', '69555344', 6); 24 | 25 | describe('Set Swap In', () => { 26 | 27 | const liquidityPool: LiquidityPool = new LiquidityPool( 28 | SundaeSwapV1.identifier, 29 | 'lovelace', 30 | asset, 31 | 3699642000000n, 32 | 78391015000000n, 33 | 'addr1', 34 | ); 35 | liquidityPool.poolFeePercent = 0.3; 36 | 37 | 38 | const swapRequest: SwapRequest = dexter.newSwapRequest() 39 | .forLiquidityPool(liquidityPool) 40 | .withSwapInToken('lovelace') 41 | .withSwapInAmount(10_000_000000n) 42 | .withSlippagePercent(1.0); 43 | 44 | it('Can calculate swap parameters', () => { 45 | expect(+swapRequest.getPriceImpactPercent().toFixed(2)).toEqual(0.27); 46 | expect(swapRequest.getEstimatedReceive()).toEqual(210_684_680649n); 47 | expect(swapRequest.getMinimumReceive()).toEqual(208_598_693711n); 48 | }); 49 | 50 | it('Can build swap order', () => { 51 | const sundaeswap: SundaeSwapV1 = new SundaeSwapV1(); 52 | const defaultSwapParameters: DatumParameters = { 53 | [DatumParameterKey.PoolIdentifier]: '1234', 54 | [DatumParameterKey.SenderPubKeyHash]: walletProvider.publicKeyHash(), 55 | [DatumParameterKey.SenderStakingKeyHash]: walletProvider.stakingKeyHash(), 56 | [DatumParameterKey.ReceiverPubKeyHash]: walletProvider.publicKeyHash(), 57 | [DatumParameterKey.ReceiverStakingKeyHash]: walletProvider.stakingKeyHash(), 58 | [DatumParameterKey.SwapInAmount]: swapRequest.swapInAmount, 59 | [DatumParameterKey.MinReceive]: swapRequest.getMinimumReceive(), 60 | [DatumParameterKey.SwapInTokenPolicyId]: '', 61 | [DatumParameterKey.SwapInTokenAssetName]: '', 62 | [DatumParameterKey.SwapOutTokenPolicyId]: asset.policyId, 63 | [DatumParameterKey.SwapOutTokenAssetName]: asset.nameHex, 64 | }; 65 | 66 | return sundaeswap.buildSwapOrder(liquidityPool, defaultSwapParameters) 67 | .then((payments: PayToAddress[]) => { 68 | expect(() => { sundaeswap.buildSwapOrder(liquidityPool, defaultSwapParameters); }).not.toThrowError(); 69 | expect(payments[0].addressType).toBe(AddressType.Contract); 70 | expect(payments[0].assetBalances[0].quantity).toEqual(10004500000n); 71 | expect(payments[0].datum).toBe('d8799f421234d8799fd8799fd8799fd8799f42ed56ffd8799fd8799fd8799f42bac6ffffffffd87a80ffd87a80ff1a002625a0d8799fd879801b00000002540be400d8799f1b000000309173774fffffff'); 72 | }); 73 | }); 74 | 75 | }); 76 | 77 | describe('Set Swap Out', () => { 78 | 79 | const liquidityPool: LiquidityPool = new LiquidityPool( 80 | SundaeSwapV1.identifier, 81 | 'lovelace', 82 | asset, 83 | 1032791394311n, 84 | 74925313821n, 85 | 'addr1', 86 | ); 87 | liquidityPool.poolFeePercent = 0.3; 88 | 89 | const swapRequest: SwapRequest = dexter.newSwapRequest() 90 | .forLiquidityPool(liquidityPool) 91 | .withSwapInToken('lovelace') 92 | .withSwapOutAmount(100_000000n) 93 | .withSlippagePercent(0.5); 94 | 95 | it('Can calculate swap parameters', () => { 96 | expect(swapRequest.swapInAmount).toEqual(1384410858n); 97 | }); 98 | 99 | }); 100 | 101 | describe('SundaeSwap Cancel Order', () => { 102 | let sundaeswap: SundaeSwapV1; 103 | const returnAddress = 'addr1'; 104 | 105 | beforeEach(() => { 106 | sundaeswap = new SundaeSwapV1(); 107 | }); 108 | 109 | it('should successfully cancel an order', async () => { 110 | let marketOrderAddress = sundaeswap.orderAddress; 111 | const txOutputs: UTxO[] = [ 112 | { 113 | txHash: 'mockTxHash123', 114 | address: marketOrderAddress, 115 | datumHash: 'mockDatumHash123', 116 | outputIndex: 0, 117 | assetBalances: [{ asset: 'lovelace', quantity: 1000000000000n }] 118 | } 119 | ]; 120 | 121 | const result = await sundaeswap.buildCancelSwapOrder(txOutputs, returnAddress); 122 | 123 | expect(result).toBeDefined(); 124 | expect(result[0].address).toBe(returnAddress); 125 | }); 126 | 127 | it('should fail to cancel an order with invalid UTxO', async () => { 128 | const invalidTxOutputs: UTxO[] = [ 129 | { 130 | txHash: 'invalidTxHash', 131 | address: 'invalidAddress', 132 | datumHash: 'invalidDatumHash', 133 | outputIndex: 0, 134 | assetBalances: [{ asset: 'lovelace', quantity: 1000000000000n }] 135 | } 136 | ]; 137 | try { 138 | await sundaeswap.buildCancelSwapOrder(invalidTxOutputs, returnAddress); 139 | fail('Expected buildCancelSwapOrder to throw an error'); 140 | } catch (error: unknown) { 141 | if (error instanceof Error) { 142 | expect(error.message).toContain('Unable to find relevant UTxO for cancelling the swap order.'); 143 | } 144 | } 145 | 146 | }); 147 | 148 | }); 149 | 150 | }); 151 | -------------------------------------------------------------------------------- /tests/swap-request.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Asset, 3 | Dexter, 4 | LiquidityPool, 5 | MockDataProvider, 6 | MockWalletProvider, 7 | SwapRequest, 8 | WingRiders, 9 | Minswap 10 | } from '../src'; 11 | 12 | describe('SwapRequest', () => { 13 | 14 | const walletProvider: MockWalletProvider = new MockWalletProvider(); 15 | walletProvider.loadWalletFromSeedPhrase(['']); 16 | const dexter: Dexter = (new Dexter()) 17 | .withDataProvider(new MockDataProvider()) 18 | .withWalletProvider(walletProvider); 19 | const asset: Asset = new Asset('f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880', '69555344', 6); 20 | 21 | describe('Parameter setting', () => { 22 | 23 | const liquidityPool: LiquidityPool = new LiquidityPool( 24 | WingRiders.identifier, 25 | 'lovelace', 26 | asset, 27 | 1_000000n, 28 | 1_000000n, 29 | 'addr1', 30 | ); 31 | const swapRequest: SwapRequest = dexter.newSwapRequest() 32 | .forLiquidityPool(liquidityPool); 33 | 34 | it('Can set liquidity pool', () => { 35 | expect(swapRequest.liquidityPool.uuid).toBe(liquidityPool.uuid); 36 | }); 37 | 38 | it('Can set swap tokens', () => { 39 | swapRequest.withSwapInToken('lovelace'); 40 | 41 | expect(swapRequest.swapInToken).toBe('lovelace'); 42 | expect((swapRequest.swapOutToken as Asset).identifier()).toBe(asset.identifier()); 43 | }); 44 | 45 | it('Fails on invalid swap in token', () => { 46 | expect(() => { swapRequest.withSwapInToken(new Asset('test1', 'test2')); }).toThrowError(); 47 | }); 48 | 49 | it('Can flip swap tokens', () => { 50 | swapRequest.withSwapInToken('lovelace') 51 | .flip(); 52 | 53 | expect((swapRequest.swapInToken as Asset).identifier()).toBe(asset.identifier()); 54 | expect(swapRequest.swapOutToken).toBe('lovelace'); 55 | }); 56 | 57 | it('Can set swap in amount', () => { 58 | swapRequest.withSwapInToken('lovelace') 59 | .withSwapInAmount(100n); 60 | 61 | expect(swapRequest.swapInAmount).toBe(100n); 62 | }); 63 | 64 | it('Fails on incorrect swap in amount', () => { 65 | swapRequest.withSwapInToken('lovelace'); 66 | swapRequest.withSwapInAmount(-1n) 67 | 68 | expect(swapRequest.swapInAmount).toBe(0n); 69 | }); 70 | 71 | it('Can set slippage percent', () => { 72 | swapRequest.withSlippagePercent(5.0); 73 | 74 | expect(swapRequest.slippagePercent).toBe(5.0); 75 | }); 76 | 77 | it('Fails on incorrect slippage percent', () => { 78 | expect(() => { swapRequest.withSlippagePercent(-5.0); }).toThrowError(); 79 | }); 80 | 81 | }); 82 | 83 | describe('Alter Order', () => { 84 | 85 | const liquidityPool: LiquidityPool = new LiquidityPool( 86 | Minswap.identifier, 87 | 'lovelace', 88 | asset, 89 | 30817255371488n, 90 | 349805856622734n, 91 | 'addr1', 92 | ); 93 | liquidityPool.poolFeePercent = 0.3; 94 | 95 | const swapRequest: SwapRequest = dexter.newSwapRequest() 96 | .forLiquidityPool(liquidityPool) 97 | .withSwapInToken('lovelace') 98 | .withSwapInAmount(10_000_000_000000n) 99 | .withSlippagePercent(0.5); 100 | 101 | it('Can flip swap in & swap out amounts', () => { 102 | swapRequest.flip(); 103 | 104 | expect(swapRequest.swapOutToken).toBe('lovelace'); 105 | expect(swapRequest.swapInAmount).toBe(168542118380811n); 106 | expect(swapRequest.getEstimatedReceive()).toBe(10_000_000_000000n); 107 | }); 108 | 109 | }); 110 | 111 | }); 112 | -------------------------------------------------------------------------------- /tests/vyfinance.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AddressType, 3 | Asset, 4 | DatumParameterKey, 5 | DatumParameters, 6 | Dexter, 7 | LiquidityPool, 8 | MockDataProvider, 9 | MockWalletProvider, 10 | PayToAddress, 11 | SwapRequest, 12 | UTxO, 13 | VyFinance, 14 | } from '../src'; 15 | 16 | describe('VyFinance', () => { 17 | 18 | const walletProvider: MockWalletProvider = new MockWalletProvider(); 19 | walletProvider.loadWalletFromSeedPhrase(['']); 20 | const dexter: Dexter = (new Dexter()) 21 | .withDataProvider(new MockDataProvider()) 22 | .withWalletProvider(walletProvider); 23 | const asset: Asset = new Asset('f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880', '69555344', 6); 24 | 25 | describe('Set Swap In', () => { 26 | 27 | const liquidityPool: LiquidityPool = new LiquidityPool( 28 | VyFinance.identifier, 29 | 'lovelace', 30 | asset, 31 | 519219742499n, 32 | 39619096012n, 33 | 'addr1', 34 | ); 35 | liquidityPool.poolFeePercent = 0.3; 36 | liquidityPool.marketOrderAddress = 'addr1testorder'; 37 | 38 | const swapRequest: SwapRequest = dexter.newSwapRequest() 39 | .forLiquidityPool(liquidityPool) 40 | .withSwapInToken('lovelace') 41 | .withSwapInAmount(100_000_000000n) 42 | .withSlippagePercent(0.5); 43 | 44 | it('Can calculate swap parameters', () => { 45 | expect(+swapRequest.getPriceImpactPercent().toFixed(2)).toEqual(16.11); 46 | expect(swapRequest.getEstimatedReceive()).toEqual(6382126148n); 47 | expect(swapRequest.getMinimumReceive()).toEqual(6350374276n); 48 | }); 49 | 50 | it('Can build swap order', () => { 51 | const vyfi: VyFinance = new VyFinance(); 52 | const defaultSwapParameters: DatumParameters = { 53 | [DatumParameterKey.SenderPubKeyHash]: walletProvider.publicKeyHash(), 54 | [DatumParameterKey.SenderStakingKeyHash]: walletProvider.stakingKeyHash(), 55 | [DatumParameterKey.ReceiverPubKeyHash]: walletProvider.publicKeyHash(), 56 | [DatumParameterKey.ReceiverStakingKeyHash]: walletProvider.stakingKeyHash(), 57 | [DatumParameterKey.SwapInAmount]: swapRequest.swapInAmount, 58 | [DatumParameterKey.MinReceive]: swapRequest.getMinimumReceive(), 59 | [DatumParameterKey.SwapInTokenPolicyId]: '', 60 | [DatumParameterKey.SwapInTokenAssetName]: '', 61 | [DatumParameterKey.SwapOutTokenPolicyId]: asset.policyId, 62 | [DatumParameterKey.SwapOutTokenAssetName]: asset.nameHex, 63 | }; 64 | 65 | return vyfi.buildSwapOrder(liquidityPool, defaultSwapParameters) 66 | .then((payments: PayToAddress[]) => { 67 | expect(() => { vyfi.buildSwapOrder(liquidityPool, defaultSwapParameters); }).not.toThrowError(); 68 | expect(payments[0].addressType).toBe(AddressType.Contract); 69 | expect(payments[0].assetBalances[0].quantity).toBe(100_003_900000n); 70 | expect(payments[0].datum).toBe('d8799f44ed56bac6d87c9f1b000000017a830584ffff'); 71 | }); 72 | }); 73 | 74 | }); 75 | 76 | describe('Set Swap Out', () => { 77 | 78 | const liquidityPool: LiquidityPool = new LiquidityPool( 79 | VyFinance.identifier, 80 | 'lovelace', 81 | asset, 82 | 519179782499n, 83 | 39622139292n, 84 | 'addr1', 85 | ); 86 | liquidityPool.poolFeePercent = 0.3; 87 | 88 | const swapRequest: SwapRequest = dexter.newSwapRequest() 89 | .forLiquidityPool(liquidityPool) 90 | .withSwapInToken('lovelace') 91 | .withSwapOutAmount(1_000_000000n) 92 | .withSlippagePercent(0.5); 93 | 94 | it('Can calculate swap parameters', () => { 95 | expect(swapRequest.swapInAmount).toEqual(13482992348n); 96 | }); 97 | 98 | }); 99 | 100 | describe('VyFinance Cancel Order', () => { 101 | let vyFinance: VyFinance; 102 | const returnAddress = 'addr1'; 103 | beforeEach(() => { 104 | vyFinance = new VyFinance(); 105 | vyFinance.api.liquidityPools = async () => { 106 | const liquidityPool = new LiquidityPool( 107 | VyFinance.identifier, 108 | 'lovelace', 109 | new Asset('f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880', '69555344'), 110 | 519219742499n, 111 | 39619096012n, 112 | 'mockPoolAddress', 113 | 'mockMarketOrderAddress', 114 | 'mockLimitOrderAddress', 115 | ); 116 | liquidityPool.extra = { 117 | nft: { 118 | policyId: 'mockNftPolicyId' 119 | } 120 | }; 121 | 122 | return [liquidityPool]; 123 | }; 124 | }); 125 | 126 | it('should successfully cancel an order', async () => { 127 | const MockUTxO: UTxO[] = [{ 128 | txHash: 'mockTxHash', 129 | address: 'mockMarketOrderAddress', 130 | datumHash: 'mockDatumHash', 131 | outputIndex: 0, 132 | assetBalances: [{asset: 'lovelace', quantity: 1000000000000n}] 133 | }]; 134 | 135 | const cancelOrder = await vyFinance.buildCancelSwapOrder(MockUTxO, returnAddress); 136 | expect(cancelOrder).toBeDefined(); 137 | expect(cancelOrder[0].address).toBe('addr1'); 138 | expect(cancelOrder[0].assetBalances[0].quantity).toBe(1000000000000n); 139 | }); 140 | 141 | it('should fail to cancel an order when the liquidity pool is not found', async () => { 142 | const mockUTxO: UTxO[] = [{ 143 | txHash: 'mockTxHash', 144 | address: 'mockAddress', 145 | datumHash: 'mockDatumHash', 146 | outputIndex: 0, 147 | assetBalances: [{asset: 'lovelace', quantity: 1000000000000n}] 148 | }]; 149 | 150 | try { 151 | await vyFinance.buildCancelSwapOrder(mockUTxO, returnAddress); 152 | fail('Expected buildCancelSwapOrder to throw an error'); 153 | } catch (error: unknown) { 154 | if (error instanceof Error) { 155 | expect(error.message).toContain('Unable to find relevant liquidity pool for cancelling the swap order.'); 156 | } 157 | } 158 | 159 | }); 160 | 161 | it('should fail to cancel an order with invalid UTxO', async () => { 162 | const invalidTxOutputs: UTxO[] = [ 163 | { 164 | txHash: 'invalidTxHash', 165 | address: 'invalidAddress', 166 | datumHash: 'invalidDatumHash', 167 | outputIndex: 0, 168 | assetBalances: [{ asset: 'lovelace', quantity: 1000000000000n }] 169 | } 170 | ]; 171 | 172 | try { 173 | await vyFinance.buildCancelSwapOrder(invalidTxOutputs, returnAddress); 174 | fail('Expected buildCancelSwapOrder to throw an error'); 175 | } catch (error: unknown) { 176 | if (error instanceof Error) { 177 | expect(error.message).toContain('Unable to find relevant UTxO for cancelling the swap order.'); 178 | } 179 | } 180 | 181 | }); 182 | 183 | }); 184 | 185 | }); 186 | -------------------------------------------------------------------------------- /tests/wingriders.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dexter, 3 | LiquidityPool, 4 | WingRiders, 5 | MockDataProvider, 6 | SwapRequest, 7 | Asset, 8 | MockWalletProvider, 9 | DatumParameters, 10 | DatumParameterKey, 11 | PayToAddress, 12 | AddressType, UTxO, 13 | } from '../src'; 14 | 15 | describe('WingRiders', () => { 16 | 17 | const walletProvider: MockWalletProvider = new MockWalletProvider(); 18 | walletProvider.loadWalletFromSeedPhrase(['']); 19 | const dexter: Dexter = (new Dexter()) 20 | .withDataProvider(new MockDataProvider()) 21 | .withWalletProvider(walletProvider); 22 | const asset: Asset = new Asset('f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880', '69555344', 6); 23 | 24 | describe('Set Swap In', () => { 25 | 26 | const liquidityPool: LiquidityPool = new LiquidityPool( 27 | WingRiders.identifier, 28 | 'lovelace', 29 | asset, 30 | 923224398616n, 31 | 7942169n, 32 | 'addr1', 33 | ); 34 | liquidityPool.poolFeePercent = 0.35; 35 | 36 | const swapRequest: SwapRequest = dexter.newSwapRequest() 37 | .forLiquidityPool(liquidityPool) 38 | .withSwapInToken('lovelace') 39 | .withSwapInAmount(10_000_000000n) 40 | .withSlippagePercent(0.5); 41 | 42 | it('Can calculate swap parameters', () => { 43 | expect(+swapRequest.getPriceImpactPercent().toFixed(2)).toEqual(1.43); 44 | expect(swapRequest.getEstimatedReceive()).toEqual(84809n); 45 | expect(swapRequest.getMinimumReceive()).toEqual(84387n); 46 | }); 47 | 48 | it('Can build swap order', () => { 49 | const wingriders: WingRiders = new WingRiders(); 50 | const defaultSwapParameters: DatumParameters = { 51 | [DatumParameterKey.SenderPubKeyHash]: walletProvider.publicKeyHash(), 52 | [DatumParameterKey.SenderStakingKeyHash]: walletProvider.stakingKeyHash(), 53 | [DatumParameterKey.ReceiverPubKeyHash]: walletProvider.publicKeyHash(), 54 | [DatumParameterKey.ReceiverStakingKeyHash]: walletProvider.stakingKeyHash(), 55 | [DatumParameterKey.SwapInAmount]: swapRequest.swapInAmount, 56 | [DatumParameterKey.MinReceive]: swapRequest.getMinimumReceive(), 57 | [DatumParameterKey.SwapInTokenPolicyId]: '', 58 | [DatumParameterKey.SwapInTokenAssetName]: '', 59 | [DatumParameterKey.SwapOutTokenPolicyId]: asset.policyId, 60 | [DatumParameterKey.SwapOutTokenAssetName]: asset.nameHex, 61 | }; 62 | 63 | return wingriders.buildSwapOrder(liquidityPool, defaultSwapParameters) 64 | .then((payments: PayToAddress[]) => { 65 | expect(() => { wingriders.buildSwapOrder(liquidityPool, defaultSwapParameters); }).not.toThrowError(); 66 | expect(payments[0].addressType).toBe(AddressType.Contract); 67 | expect(payments[0].assetBalances[0].quantity).toEqual(10004000000n); 68 | }); 69 | }); 70 | 71 | it('Can calculate price impact with 0 decimals', () => { 72 | const hosky: Asset = new Asset('a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235', '484f534b59', 0); 73 | const hoskyPool: LiquidityPool = new LiquidityPool( 74 | WingRiders.identifier, 75 | 'lovelace', 76 | hosky, 77 | 52428070796n, 78 | 1424861277563n, 79 | 'addr1', 80 | ); 81 | hoskyPool.poolFeePercent = 0.35; 82 | 83 | const swap: SwapRequest = dexter.newSwapRequest() 84 | .forLiquidityPool(hoskyPool) 85 | .withSwapInToken('lovelace') 86 | .withSwapInAmount(1_000_000000n) 87 | .withSlippagePercent(0.5); 88 | 89 | expect(+swap.getPriceImpactPercent().toFixed(2)).toEqual(2.23); 90 | }); 91 | 92 | }); 93 | 94 | describe('Set Swap Out', () => { 95 | 96 | const liquidityPool: LiquidityPool = new LiquidityPool( 97 | WingRiders.identifier, 98 | 'lovelace', 99 | asset, 100 | 925723148616n, 101 | 7920796n, 102 | 'addr1', 103 | ); 104 | liquidityPool.poolFeePercent = 0.35; 105 | 106 | const swapRequest: SwapRequest = dexter.newSwapRequest() 107 | .forLiquidityPool(liquidityPool) 108 | .withSwapInToken('lovelace') 109 | .withSwapOutAmount(1_000000n) 110 | .withSlippagePercent(0.5); 111 | 112 | it('Can calculate swap parameters', () => { 113 | expect(swapRequest.swapInAmount).toEqual(134229438286n); 114 | }); 115 | 116 | }); 117 | 118 | describe('Wingriders Cancel Order', () => { 119 | let wingRiders: WingRiders; 120 | const returnAddress = 'addr1'; 121 | beforeEach(() => { 122 | wingRiders = new WingRiders(); 123 | }); 124 | 125 | it('should successfully cancel an order', async () => { 126 | let marketOrderAddress = wingRiders.orderAddress; 127 | const txOutputs: UTxO[] = [ 128 | { 129 | txHash: 'mockTxHash123', 130 | address: marketOrderAddress, 131 | datumHash: 'mockDatumHash123', 132 | outputIndex: 0, 133 | assetBalances: [{ asset: 'lovelace', quantity: 1000000000000n }] 134 | } 135 | ]; 136 | 137 | const result = await wingRiders.buildCancelSwapOrder(txOutputs, returnAddress); 138 | 139 | expect(result).toBeDefined(); 140 | expect(result[0].address).toBe(returnAddress); 141 | }); 142 | 143 | it('should fail to cancel an order with invalid UTxO', async () => { 144 | const invalidTxOutputs: UTxO[] = [ 145 | { 146 | txHash: 'invalidTxHash', 147 | address: 'invalidAddress', 148 | datumHash: 'invalidDatumHash', 149 | outputIndex: 0, 150 | assetBalances: [{ asset: 'lovelace', quantity: 1000000000000n }] 151 | } 152 | ]; 153 | try { 154 | await wingRiders.buildCancelSwapOrder(invalidTxOutputs, returnAddress); 155 | fail('Expected buildCancelSwapOrder to throw an error'); 156 | } catch (error: unknown) { 157 | if (error instanceof Error) { 158 | expect(error.message).toContain('Unable to find relevant UTxO for cancelling the swap order.'); 159 | } 160 | } 161 | 162 | }); 163 | 164 | }); 165 | 166 | }); 167 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build", 4 | "baseUrl": ".", 5 | "module": "es2020", 6 | "target": "es2020", 7 | "lib": ["es2020", "DOM"], 8 | "moduleResolution": "node", 9 | "allowJs": true, 10 | "noImplicitAny": true, 11 | "declaration": true, 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictBindCallApply": true, 16 | "strictPropertyInitialization": false, 17 | "noImplicitThis": true, 18 | "alwaysStrict": false, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "esModuleInterop": true, 24 | "skipLibCheck": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "resolveJsonModule": true, 27 | "allowSyntheticDefaultImports": true, 28 | "paths": { 29 | "@app/*": ["src/*"], 30 | "@dex/*": ["src/dex/*"], 31 | "@providers/*": ["src/providers/*"], 32 | "@requests/*": ["src/requests/*"], 33 | } 34 | }, 35 | "ts-node": { 36 | "compilerOptions": { 37 | "module": "CommonJS" 38 | } 39 | }, 40 | "include": ["src/**/*.ts"], 41 | "exclude": ["node_modules", "tests", "build"], 42 | } 43 | --------------------------------------------------------------------------------