├── .gitignore
├── .prettierrc
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── app
├── .babelrc
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── README.md
├── next.config.js
├── package.json
├── postcss.config.js
├── public
│ ├── arb1.png
│ ├── arb2.jpeg
│ ├── arb2.png
│ └── favicon.ico
├── src
│ ├── components
│ │ ├── AppBar.tsx
│ │ ├── ArbCard.tsx
│ │ ├── ContentContainer.tsx
│ │ ├── Footer.tsx
│ │ ├── NetworkSwitcher.tsx
│ │ ├── Notification.tsx
│ │ ├── RequestAirdrop.tsx
│ │ ├── SendTransaction.tsx
│ │ ├── SendVersionedTransaction.tsx
│ │ ├── SignMessage.tsx
│ │ ├── Text
│ │ │ └── index.tsx
│ │ └── nav-element
│ │ │ └── index.tsx
│ ├── contexts
│ │ ├── AutoConnectProvider.tsx
│ │ ├── ContextProvider.tsx
│ │ └── NetworkConfigurationProvider.tsx
│ ├── idl
│ │ ├── swap_program.json
│ │ └── swap_program.ts
│ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── api
│ │ │ └── hello.ts
│ │ └── index.tsx
│ ├── stores
│ │ ├── useAssetsStore.tsx
│ │ ├── useNotificationStore.tsx
│ │ └── useUserSOLBalanceStore.tsx
│ ├── styles
│ │ └── globals.css
│ ├── utils
│ │ ├── const.ts
│ │ ├── explorer.ts
│ │ ├── index.tsx
│ │ ├── notifications.tsx
│ │ └── program.ts
│ └── views
│ │ ├── home
│ │ └── index.tsx
│ │ └── index.tsx
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
├── package.json
├── program
├── Cargo.toml
└── src
│ ├── arb.rs
│ ├── error.rs
│ ├── lib.rs
│ ├── partial_state.rs
│ ├── processor.rs
│ ├── swap.rs
│ └── util.rs
├── rustfmt.toml
├── tests
├── main.test.ts
└── util
│ ├── assets.json
│ ├── const.ts
│ ├── index.ts
│ ├── instruction.ts
│ ├── token.ts
│ └── transaction.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | node_modules
4 | test-ledger
5 | target
6 |
7 | yarn-error.log
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "printWidth": 80,
5 | "tabWidth": 4
6 | }
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "program"
4 | ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | Copyright 2024 Joe Caulfield
179 |
180 | Licensed under the Apache License, Version 2.0 (the "License");
181 | you may not use this file except in compliance with the License.
182 | You may obtain a copy of the License at
183 |
184 | http://www.apache.org/licenses/LICENSE-2.0
185 |
186 | Unless required by applicable law or agreed to in writing, software
187 | distributed under the License is distributed on an "AS IS" BASIS,
188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189 | See the License for the specific language governing permissions and
190 | limitations under the License.
191 |
--------------------------------------------------------------------------------
/app/.babelrc:
--------------------------------------------------------------------------------
1 | { "presets": ["next/babel"] }
2 |
--------------------------------------------------------------------------------
/app/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/app/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "printWidth": 80,
5 | "tabWidth": 4
6 | }
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | ```
14 |
15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
16 |
17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
18 |
19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
20 |
21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
22 |
23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
24 |
25 | ## Learn More
26 |
27 | To learn more about Next.js, take a look at the following resources:
28 |
29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
31 |
32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
33 |
34 | ## Deploy on Vercel
35 |
36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
37 |
38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
39 |
--------------------------------------------------------------------------------
/app/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | images: {
5 | remotePatterns: [
6 | {
7 | protocol: 'https',
8 | hostname: 'arweave.net',
9 | port: '',
10 | pathname: '/*',
11 | },
12 | ],
13 | },
14 | }
15 |
16 | module.exports = nextConfig
17 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "swap-ui",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@heroicons/react": "v1",
13 | "@metaplex-foundation/mpl-token-metadata": "^2.11.1",
14 | "@noble/ed25519": "^2.0.0",
15 | "@solana/spl-token": "^0.3.7",
16 | "@solana/wallet-adapter-base": "^0.9.22",
17 | "@solana/wallet-adapter-react": "^0.15.32",
18 | "@solana/wallet-adapter-react-ui": "^0.9.31",
19 | "@solana/wallet-adapter-wallets": "^0.19.16",
20 | "@solana/web3.js": "^1.76.0",
21 | "@types/node": "20.1.4",
22 | "@types/react": "18.2.6",
23 | "@types/react-dom": "18.2.4",
24 | "bs58": "^5.0.0",
25 | "daisyui": "^2.51.6",
26 | "date-fns": "^2.30.0",
27 | "eslint": "8.40.0",
28 | "eslint-config-next": "13.4.2",
29 | "immer": "^10.0.2",
30 | "next": "13.4.2",
31 | "next-compose-plugins": "^2.2.1",
32 | "next-transpile-modules": "^10.0.0",
33 | "react": "18.2.0",
34 | "react-dom": "18.2.0",
35 | "react-icons": "^4.8.0",
36 | "typescript": "5.0.4",
37 | "zustand": "^4.3.8"
38 | },
39 | "devDependencies": {
40 | "@tailwindcss/typography": "^0.5.9",
41 | "autoprefixer": "^10.4.14",
42 | "postcss": "^8.4.23",
43 | "prettier": "^2.8.8",
44 | "tailwindcss": "^3.3.2"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/app/public/arb1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buffalojoec/arb-program/fab6007996d332aebb406c5b6e3dd9171c5785c6/app/public/arb1.png
--------------------------------------------------------------------------------
/app/public/arb2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buffalojoec/arb-program/fab6007996d332aebb406c5b6e3dd9171c5785c6/app/public/arb2.jpeg
--------------------------------------------------------------------------------
/app/public/arb2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buffalojoec/arb-program/fab6007996d332aebb406c5b6e3dd9171c5785c6/app/public/arb2.png
--------------------------------------------------------------------------------
/app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buffalojoec/arb-program/fab6007996d332aebb406c5b6e3dd9171c5785c6/app/public/favicon.ico
--------------------------------------------------------------------------------
/app/src/components/AppBar.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic'
2 | import Link from 'next/link'
3 | import React, { useState } from 'react'
4 | import { useAutoConnect } from '@/contexts/AutoConnectProvider'
5 | import NavElement from '@/components/nav-element'
6 | import NetworkSwitcher from '@/components/NetworkSwitcher'
7 |
8 | const WalletMultiButtonDynamic = dynamic(
9 | async () =>
10 | (await import('@solana/wallet-adapter-react-ui')).WalletMultiButton,
11 | { ssr: false }
12 | )
13 |
14 | export const AppBar: React.FC = () => {
15 | const { autoConnect, setAutoConnect } = useAutoConnect()
16 | const [isNavOpen, setIsNavOpen] = useState(false)
17 | return (
18 |
19 | {/* NavBar / Header */}
20 |
21 |
22 |
23 |
30 |
37 |
38 |
42 |
46 |
50 |
54 |
58 |
62 |
66 |
67 |
68 |
76 |
80 |
84 |
88 |
92 |
96 |
100 |
101 |
102 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | There's arbitrage opportunities out there for the
118 | real scallywags...
119 |
120 |
121 |
122 | {/* Nav Links */}
123 | {/* Wallet & Settings */}
124 |
125 |
126 |
127 |
128 |
setIsNavOpen(!isNavOpen)}
132 | >
133 |
150 |
156 |
162 |
163 |
164 |
165 |
166 |
167 |
171 |
178 |
184 |
190 |
191 |
192 |
196 |
197 |
198 |
199 | Autoconnect
200 |
204 | setAutoConnect(e.target.checked)
205 | }
206 | className="toggle"
207 | />
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | )
218 | }
219 |
--------------------------------------------------------------------------------
/app/src/components/ArbCard.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { AiFillCheckCircle } from 'react-icons/ai'
3 | import { GiPirateFlag } from 'react-icons/gi'
4 |
5 | const MyComponent = () => {
6 | const [swap1, setSwap1] = useState('')
7 | const [swap2, setSwap2] = useState('')
8 | const [concurrency, setConcurrency] = useState(2)
9 | const [temperature, setTemperature] = useState(1)
10 | const [arbitrageRunning, setArbitragerunning] = useState(false)
11 | const [logs, setLogs] = useState([])
12 |
13 | const handleSwap1Change = (e: {
14 | target: { value: React.SetStateAction }
15 | }) => {
16 | setSwap1(e.target.value)
17 | }
18 |
19 | const handleSwap2Change = (e: {
20 | target: { value: React.SetStateAction }
21 | }) => {
22 | setSwap2(e.target.value)
23 | }
24 |
25 | const handleConcurrencyChange = (e: { target: { value: string } }) => {
26 | const value = parseInt(e.target.value)
27 | setConcurrency(value)
28 | }
29 |
30 | const handleTemperatureChange = (e: { target: { value: string } }) => {
31 | const value = parseInt(e.target.value)
32 | setTemperature(value)
33 | }
34 |
35 | const logToTerminal = (message: string) => {
36 | setLogs((prevLogs: string[]) => [...prevLogs, message])
37 | }
38 |
39 | const handleLaunchArbitrage = () => {
40 | setArbitragerunning(true)
41 | }
42 |
43 | const handleStopArbitrage = () => setArbitragerunning(false)
44 |
45 | const executeArbitrage = async () => {
46 | logToTerminal('Hello')
47 | await new Promise((resolve) => setTimeout(resolve, 2 * 1000))
48 | }
49 |
50 | // useEffect(() => {
51 | // while (arbitrageRunning) {
52 | // executeArbitrage()
53 | // }
54 | // }, [arbitrageRunning])
55 |
56 | return (
57 |
58 |
59 |
127 |
128 |
135 |
140 |
141 |
146 |
147 | Launch Arbitrage
148 |
149 |
150 |
151 |
152 |
159 |
164 |
165 |
170 |
171 | Stop Arbitrage
172 |
173 |
174 |
175 |
176 |
177 |
178 | {logs.length > 0 && (
179 |
180 | {logs.map((log, index) => (
181 |
{log}
182 | ))}
183 |
184 | )}
185 |
186 | )
187 | }
188 |
189 | export default MyComponent
190 |
--------------------------------------------------------------------------------
/app/src/components/ContentContainer.tsx:
--------------------------------------------------------------------------------
1 | import Text from './Text'
2 | import NavElement from '@/components/nav-element'
3 | interface Props {
4 | children: React.ReactNode
5 | }
6 |
7 | export const ContentContainer: React.FC = ({ children }) => {
8 | return (
9 |
10 |
15 |
{children}
16 | {/* SideBar / Drawer */}
17 |
18 |
22 |
23 |
24 |
25 |
29 | Menu
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import Link from 'next/link';
3 | import Image from 'next/image';
4 | export const Footer: FC = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
21 |
22 |
23 |
57 |
58 | © 2023 Solana Foundation
59 |
60 |
61 |
62 |
63 |
SOLANA
64 |
65 |
66 |
67 | Labs
68 |
69 |
70 | Foundation
71 |
72 |
73 | Solana Mobile
74 |
75 |
76 | Solana Pay
77 |
78 |
79 | Grants
80 |
81 |
82 |
83 |
84 |
85 |
DEVELOPERS
86 |
87 |
88 |
89 | Documentation
90 |
91 |
92 | Mobile SDK
93 |
94 |
95 | Pay SDK
96 |
97 |
98 | Cookbook
99 |
100 |
101 | DAOs
102 |
103 |
104 |
105 |
106 |
107 |
ECOSYSTEM
108 |
109 |
110 |
111 | News
112 |
113 |
114 | Validators
115 |
116 |
117 | Youtube
118 |
119 |
120 | Realms
121 |
122 |
123 | Solana U
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | );
132 | };
133 |
--------------------------------------------------------------------------------
/app/src/components/NetworkSwitcher.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import dynamic from 'next/dynamic';
3 | import { useNetworkConfiguration } from '@/contexts/NetworkConfigurationProvider';
4 |
5 | const NetworkSwitcher: FC = () => {
6 | const { networkConfiguration, setNetworkConfiguration } = useNetworkConfiguration();
7 |
8 | console.log(networkConfiguration);
9 |
10 | return (
11 |
12 | Network
13 | setNetworkConfiguration(e.target.value)}
16 | className="select max-w-xs"
17 | >
18 | main
19 | dev
20 | test
21 |
22 |
23 | );
24 | };
25 |
26 | export default dynamic(() => Promise.resolve(NetworkSwitcher), {
27 | ssr: false
28 | })
--------------------------------------------------------------------------------
/app/src/components/Notification.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import {
3 | CheckCircleIcon,
4 | InformationCircleIcon,
5 | XCircleIcon,
6 | } from '@heroicons/react/outline'
7 | import { XIcon } from '@heroicons/react/solid'
8 | import useNotificationStore from '../stores/useNotificationStore'
9 | import { useConnection } from '@solana/wallet-adapter-react'
10 | import { getExplorerUrl } from '../utils/explorer'
11 | import { useNetworkConfiguration } from '@/contexts/NetworkConfigurationProvider'
12 |
13 | const NotificationList = () => {
14 | const { notifications, set: setNotificationStore } = useNotificationStore(
15 | (s) => s
16 | )
17 |
18 | const reversedNotifications = [...notifications].reverse()
19 |
20 | return (
21 |
24 |
25 | {reversedNotifications.map((n, idx) => (
26 | {
33 | setNotificationStore((state: any) => {
34 | const reversedIndex =
35 | reversedNotifications.length - 1 - idx
36 | state.notifications = [
37 | ...notifications.slice(0, reversedIndex),
38 | ...notifications.slice(reversedIndex + 1),
39 | ]
40 | })
41 | }}
42 | />
43 | ))}
44 |
45 |
46 | )
47 | }
48 |
49 | interface NotificationProps {
50 | type: string
51 | message: string
52 | description: string | undefined
53 | txid: string | undefined
54 | onHide: () => void
55 | }
56 |
57 | const Notification = (props: NotificationProps) => {
58 | const { connection } = useConnection()
59 | const { networkConfiguration } = useNetworkConfiguration()
60 |
61 | // TODO: we dont have access to the network or endpoint here..
62 | // getExplorerUrl(connection., txid, 'tx')
63 | // Either a provider, context, and or wallet adapter related pro/contx need updated
64 |
65 | useEffect(() => {
66 | const id = setTimeout(() => {
67 | props.onHide()
68 | }, 8000)
69 |
70 | return () => {
71 | clearInterval(id)
72 | }
73 | }, [props])
74 |
75 | return (
76 |
79 |
80 |
81 |
82 | {props.type === 'success' ? (
83 |
86 | ) : null}
87 | {props.type === 'info' && (
88 |
91 | )}
92 | {props.type === 'error' && (
93 |
94 | )}
95 |
96 |
97 |
98 | {props.message}
99 |
100 | {props.description ? (
101 |
102 | {props.description}
103 |
104 | ) : null}
105 | {props.txid ? (
106 |
139 | ) : null}
140 |
141 |
142 | props.onHide()}
144 | className={`bg-bkg-2 default-transition rounded-md inline-flex text-fgd-3 hover:text-fgd-4 focus:outline-none`}
145 | >
146 | Close
147 |
148 |
149 |
150 |
151 |
152 |
153 | )
154 | }
155 |
156 | export default NotificationList
157 |
--------------------------------------------------------------------------------
/app/src/components/RequestAirdrop.tsx:
--------------------------------------------------------------------------------
1 | import { useConnection, useWallet } from '@solana/wallet-adapter-react';
2 | import { LAMPORTS_PER_SOL, TransactionSignature } from '@solana/web3.js';
3 | import { FC, useCallback } from 'react';
4 | import useUserSOLBalanceStore from '@/stores/useUserSOLBalanceStore';
5 | import { notify } from '@/utils/notifications';
6 |
7 | export const RequestAirdrop: FC = () => {
8 | const { connection } = useConnection();
9 | const { publicKey } = useWallet();
10 | const { getUserSOLBalance } = useUserSOLBalanceStore();
11 |
12 | const onClick = useCallback(async () => {
13 | if (!publicKey) {
14 | console.log('error', 'Wallet not connected!');
15 | notify({
16 | type: 'error',
17 | message: 'error',
18 | description: 'Wallet not connected!',
19 | });
20 | return;
21 | }
22 |
23 | let signature: TransactionSignature = '';
24 |
25 | try {
26 | signature = await connection.requestAirdrop(
27 | publicKey,
28 | LAMPORTS_PER_SOL,
29 | );
30 |
31 | // Get the lates block hash to use on our transaction and confirmation
32 | let latestBlockhash = await connection.getLatestBlockhash();
33 | await connection.confirmTransaction(
34 | { signature, ...latestBlockhash },
35 | 'confirmed',
36 | );
37 |
38 | notify({
39 | type: 'success',
40 | message: 'Airdrop successful!',
41 | txid: signature,
42 | })
43 |
44 | getUserSOLBalance(publicKey, connection);
45 | } catch (error: any) {
46 | notify({
47 | type: 'error',
48 | message: `Airdrop failed!`,
49 | description: error?.message,
50 | txid: signature,
51 | });
52 | console.log(
53 | 'error',
54 | `Airdrop failed! ${error?.message}`,
55 | signature,
56 | );
57 | }
58 | }, [publicKey, connection, getUserSOLBalance]);
59 |
60 | return (
61 |
62 |
63 |
67 |
71 | Airdrop 1
72 |
73 |
74 |
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/app/src/components/SendTransaction.tsx:
--------------------------------------------------------------------------------
1 | import { useConnection, useWallet } from '@solana/wallet-adapter-react';
2 | import { Keypair, SystemProgram, Transaction, TransactionMessage, TransactionSignature, VersionedTransaction } from '@solana/web3.js';
3 | import { FC, useCallback } from 'react';
4 | import { notify } from "@/utils/notifications";
5 |
6 | export const SendTransaction: FC = () => {
7 | const { connection } = useConnection();
8 | const { publicKey, sendTransaction } = useWallet();
9 |
10 | const onClick = useCallback(async () => {
11 | if (!publicKey) {
12 | notify({ type: 'error', message: `Wallet not connected!` });
13 | console.log('error', `Send Transaction: Wallet not connected!`);
14 | return;
15 | }
16 |
17 | let signature: TransactionSignature = '';
18 | try {
19 |
20 | // Create instructions to send, in this case a simple transfer
21 | const instructions = [
22 | SystemProgram.transfer({
23 | fromPubkey: publicKey,
24 | toPubkey: Keypair.generate().publicKey,
25 | lamports: 1_000_000,
26 | }),
27 | ];
28 |
29 | // Get the lates block hash to use on our transaction and confirmation
30 | let latestBlockhash = await connection.getLatestBlockhash()
31 |
32 | // Create a new TransactionMessage with version and compile it to legacy
33 | const messageLegacy = new TransactionMessage({
34 | payerKey: publicKey,
35 | recentBlockhash: latestBlockhash.blockhash,
36 | instructions,
37 | }).compileToLegacyMessage();
38 |
39 | // Create a new VersionedTransacction which supports legacy and v0
40 | const transation = new VersionedTransaction(messageLegacy)
41 |
42 | // Send transaction and await for signature
43 | signature = await sendTransaction(transation, connection);
44 |
45 | // Send transaction and await for signature
46 | await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed');
47 |
48 | console.log(signature);
49 | notify({ type: 'success', message: 'Transaction successful!', txid: signature });
50 | } catch (error: any) {
51 | notify({ type: 'error', message: `Transaction failed!`, description: error?.message, txid: signature });
52 | console.log('error', `Transaction failed! ${error?.message}`, signature);
53 | return;
54 | }
55 | }, [publicKey, notify, connection, sendTransaction]);
56 |
57 | return (
58 |
59 |
60 |
62 |
66 |
67 | Wallet not connected
68 |
69 |
70 | Send Transaction
71 |
72 |
73 |
74 |
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/app/src/components/SendVersionedTransaction.tsx:
--------------------------------------------------------------------------------
1 | import { useConnection, useWallet } from '@solana/wallet-adapter-react';
2 | import { Keypair, SystemProgram, TransactionMessage, TransactionSignature, VersionedTransaction } from '@solana/web3.js';
3 | import { FC, useCallback } from 'react';
4 | import { notify } from "@/utils/notifications";
5 |
6 | export const SendVersionedTransaction: FC = () => {
7 | const { connection } = useConnection();
8 | const { publicKey, sendTransaction } = useWallet();
9 |
10 | const onClick = useCallback(async () => {
11 | if (!publicKey) {
12 | notify({ type: 'error', message: `Wallet not connected!` });
13 | console.log('error', `Send Transaction: Wallet not connected!`);
14 | return;
15 | }
16 |
17 | let signature: TransactionSignature = '';
18 | try {
19 |
20 | // Create instructions to send, in this case a simple transfer
21 | const instructions = [
22 | SystemProgram.transfer({
23 | fromPubkey: publicKey,
24 | toPubkey: Keypair.generate().publicKey,
25 | lamports: 1_000_000,
26 | }),
27 | ];
28 |
29 | // Get the lates block hash to use on our transaction and confirmation
30 | let latestBlockhash = await connection.getLatestBlockhash()
31 |
32 | // Create a new TransactionMessage with version and compile it to version 0
33 | const messageV0 = new TransactionMessage({
34 | payerKey: publicKey,
35 | recentBlockhash: latestBlockhash.blockhash,
36 | instructions,
37 | }).compileToV0Message();
38 |
39 | // Create a new VersionedTransacction to support the v0 message
40 | const transation = new VersionedTransaction(messageV0)
41 |
42 | // Send transaction and await for signature
43 | signature = await sendTransaction(transation, connection);
44 |
45 | // Await for confirmation
46 | await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed');
47 |
48 | console.log(signature);
49 | notify({ type: 'success', message: 'Transaction successful!', txid: signature });
50 | } catch (error: any) {
51 | notify({ type: 'error', message: `Transaction failed!`, description: error?.message, txid: signature });
52 | console.log('error', `Transaction failed! ${error?.message}`, signature);
53 | return;
54 | }
55 | }, [publicKey, notify, connection, sendTransaction]);
56 |
57 | return (
58 |
59 |
60 |
62 |
66 |
67 | Wallet not connected
68 |
69 |
70 | Send Versioned Transaction
71 |
72 |
73 |
74 |
75 | );
76 | };
--------------------------------------------------------------------------------
/app/src/components/SignMessage.tsx:
--------------------------------------------------------------------------------
1 | // TODO: SignMessage
2 | import { verify } from '@noble/ed25519';
3 | import { useWallet } from '@solana/wallet-adapter-react';
4 | import bs58 from 'bs58';
5 | import { FC, useCallback } from 'react';
6 | import { notify } from "@/utils/notifications";
7 |
8 | export const SignMessage: FC = () => {
9 | const { publicKey, signMessage } = useWallet();
10 |
11 | const onClick = useCallback(async () => {
12 | try {
13 | // `publicKey` will be null if the wallet isn't connected
14 | if (!publicKey) throw new Error('Wallet not connected!');
15 | // `signMessage` will be undefined if the wallet doesn't support it
16 | if (!signMessage) throw new Error('Wallet does not support message signing!');
17 | // Encode anything as bytes
18 | const message = new TextEncoder().encode('Hello, world!');
19 | // Sign the bytes using the wallet
20 | const signature = await signMessage(message);
21 | // Verify that the bytes were signed using the private key that matches the known public key
22 | if (!verify(signature, message, publicKey.toBytes())) throw new Error('Invalid signature!');
23 | notify({ type: 'success', message: 'Sign message successful!', txid: bs58.encode(signature) });
24 | } catch (error: any) {
25 | notify({ type: 'error', message: `Sign Message failed!`, description: error?.message });
26 | console.log('error', `Sign Message failed! ${error?.message}`);
27 | }
28 | }, [publicKey, notify, signMessage]);
29 |
30 | return (
31 |
32 |
33 |
35 |
39 |
40 | Wallet not connected
41 |
42 |
43 | Sign Message
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/app/src/components/Text/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import React from 'react'
3 | import { cn } from '@/utils'
4 |
5 | /**
6 | * Properties for a card component.
7 | */
8 | type TextProps = {
9 | variant:
10 | | 'heading'
11 | | 'sub-heading'
12 | | 'nav-heading'
13 | | 'nav'
14 | | 'input'
15 | | 'label'
16 | className?: string
17 | href?: string
18 | children?: React.ReactNode
19 | id?: string
20 | }
21 |
22 | /**
23 | * Pre-defined styling, according to agreed-upon design-system.
24 | */
25 | const variants = {
26 | heading: 'text-3xl font-medium',
27 | 'sub-heading': 'text-2xl font-medium',
28 | 'nav-heading': 'text-lg font-medium sm:text-xl',
29 | nav: 'font-medium',
30 | paragraph: 'text-lg',
31 | 'sub-paragraph': 'text-base font-medium text-inherit',
32 | input: 'text-sm uppercase tracking-wide',
33 | label: 'text-xs uppercase tracking-wide',
34 | }
35 |
36 | /**
37 | * Definition of a card component,the main purpose of
38 | * which is to neatly display information. Can be both
39 | * interactive and static.
40 | *
41 | * @param variant Variations relating to pre-defined styling of the element.
42 | * @param className Custom classes to be applied to the element.
43 | * @param children Child elements to be rendered within the component.
44 | */
45 | const Text = ({ variant, className, href, children }: TextProps) => (
46 |
47 | {href ? (
48 |
52 | {children}
53 |
54 | ) : (
55 | children
56 | )}
57 |
58 | )
59 |
60 | export default Text
61 |
--------------------------------------------------------------------------------
/app/src/components/nav-element/index.tsx:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-empty */
2 | import Link from 'next/link'
3 | import Text from '../Text'
4 | import { cn } from '../../utils'
5 | import { useRouter } from 'next/router'
6 | import { useEffect, useRef } from 'react'
7 |
8 | type NavElementProps = {
9 | label: string
10 | href: string
11 | as?: string
12 | scroll?: boolean
13 | chipLabel?: string
14 | disabled?: boolean
15 | navigationStarts?: () => void
16 | }
17 |
18 | const NavElement = ({
19 | label,
20 | href,
21 | as,
22 | scroll,
23 | disabled,
24 | navigationStarts = () => {},
25 | }: NavElementProps) => {
26 | const router = useRouter()
27 | const isActive = href === router.asPath || (as && as === router.asPath)
28 | const divRef = useRef(null)
29 |
30 | useEffect(() => {
31 | if (divRef.current) {
32 | divRef.current.className = cn(
33 | 'h-0.5 w-1/4 transition-all duration-300 ease-out',
34 | isActive
35 | ? '!w-full bg-gradient-to-l from-fuchsia-500 to-pink-500 '
36 | : 'group-hover:w-1/2 group-hover:bg-fuchsia-500'
37 | )
38 | }
39 | }, [isActive])
40 |
41 | return (
42 | navigationStarts()}
54 | >
55 |
56 | {label}
57 |
58 |
59 |
60 | )
61 | }
62 |
63 | export default NavElement
64 |
--------------------------------------------------------------------------------
/app/src/contexts/AutoConnectProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useLocalStorage } from '@solana/wallet-adapter-react';
2 | import { createContext, FC, ReactNode, useContext } from 'react';
3 |
4 | export interface AutoConnectContextState {
5 | autoConnect: boolean;
6 | setAutoConnect(autoConnect: boolean): void;
7 | }
8 |
9 | export const AutoConnectContext = createContext({} as AutoConnectContextState);
10 |
11 | export function useAutoConnect(): AutoConnectContextState {
12 | return useContext(AutoConnectContext);
13 | }
14 |
15 | export const AutoConnectProvider: FC<{ children: ReactNode }> = ({ children }) => {
16 | // TODO: fix auto connect to actual reconnect on refresh/other.
17 | // TODO: make switch/slider settings
18 | // const [autoConnect, setAutoConnect] = useLocalStorage('autoConnect', false);
19 | const [autoConnect, setAutoConnect] = useLocalStorage('autoConnect', true);
20 |
21 | return (
22 | {children}
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/app/src/contexts/ContextProvider.tsx:
--------------------------------------------------------------------------------
1 | import { WalletAdapterNetwork, WalletError } from '@solana/wallet-adapter-base';
2 | import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
3 | import {
4 | PhantomWalletAdapter,
5 | SolflareWalletAdapter,
6 | SolletExtensionWalletAdapter,
7 | SolletWalletAdapter,
8 | TorusWalletAdapter,
9 | // LedgerWalletAdapter,
10 | // SlopeWalletAdapter,
11 | } from '@solana/wallet-adapter-wallets';
12 | import { Cluster, clusterApiUrl } from '@solana/web3.js';
13 | import { FC, ReactNode, useCallback, useMemo } from 'react';
14 | import { AutoConnectProvider, useAutoConnect } from '@/contexts/AutoConnectProvider';
15 | import { notify } from "@/utils/notifications";
16 | import { NetworkConfigurationProvider, useNetworkConfiguration } from '@/contexts/NetworkConfigurationProvider';
17 | import dynamic from "next/dynamic";
18 |
19 | const ReactUIWalletModalProviderDynamic = dynamic(
20 | async () =>
21 | (await import("@solana/wallet-adapter-react-ui")).WalletModalProvider,
22 | { ssr: false }
23 | );
24 |
25 | const WalletContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
26 | const { autoConnect } = useAutoConnect();
27 | const { networkConfiguration } = useNetworkConfiguration();
28 | const network = networkConfiguration as WalletAdapterNetwork;
29 | const endpoint = useMemo(() => clusterApiUrl(network), [network]);
30 |
31 | console.log(network);
32 |
33 | const wallets = useMemo(
34 | () => [
35 | new PhantomWalletAdapter(),
36 | new SolflareWalletAdapter(),
37 | new SolletWalletAdapter({ network }),
38 | new SolletExtensionWalletAdapter({ network }),
39 | new TorusWalletAdapter(),
40 | // new LedgerWalletAdapter(),
41 | // new SlopeWalletAdapter(),
42 | ],
43 | [network]
44 | );
45 |
46 | const onError = useCallback(
47 | (error: WalletError) => {
48 | notify({ type: 'error', message: error.message ? `${error.name}: ${error.message}` : error.name });
49 | console.error(error);
50 | },
51 | []
52 | );
53 |
54 | return (
55 | // TODO: updates needed for updating and referencing endpoint: wallet adapter rework
56 |
57 |
58 |
59 | {children}
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export const ContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
67 | return (
68 | <>
69 |
70 |
71 | {children}
72 |
73 |
74 | >
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/app/src/contexts/NetworkConfigurationProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useLocalStorage } from '@solana/wallet-adapter-react';
2 | import { createContext, FC, ReactNode, useContext } from 'react';
3 |
4 |
5 | export interface NetworkConfigurationState {
6 | networkConfiguration: string;
7 | setNetworkConfiguration(networkConfiguration: string): void;
8 | }
9 |
10 | export const NetworkConfigurationContext = createContext({} as NetworkConfigurationState);
11 |
12 | export function useNetworkConfiguration(): NetworkConfigurationState {
13 | return useContext(NetworkConfigurationContext);
14 | }
15 |
16 | export const NetworkConfigurationProvider: FC<{ children: ReactNode }> = ({ children }) => {
17 | const [networkConfiguration, setNetworkConfiguration] = useLocalStorage("network", "devnet");
18 |
19 | return (
20 | {children}
21 | );
22 | };
--------------------------------------------------------------------------------
/app/src/idl/swap_program.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.1.0",
3 | "name": "swap_program",
4 | "instructions": [
5 | {
6 | "name": "createPool",
7 | "docs": [
8 | "Initialize the program by creating the liquidity pool"
9 | ],
10 | "accounts": [
11 | {
12 | "name": "pool",
13 | "isMut": true,
14 | "isSigner": false,
15 | "docs": [
16 | "Liquidity Pool"
17 | ],
18 | "pda": {
19 | "seeds": [
20 | {
21 | "kind": "const",
22 | "type": "string",
23 | "value": "liquidity_pool"
24 | }
25 | ]
26 | }
27 | },
28 | {
29 | "name": "payer",
30 | "isMut": true,
31 | "isSigner": true,
32 | "docs": [
33 | "Rent payer"
34 | ]
35 | },
36 | {
37 | "name": "systemProgram",
38 | "isMut": false,
39 | "isSigner": false,
40 | "docs": [
41 | "System Program: Required for creating the Liquidity Pool"
42 | ]
43 | }
44 | ],
45 | "args": []
46 | },
47 | {
48 | "name": "fundPool",
49 | "docs": [
50 | "Provide liquidity to the pool by funding it with some asset"
51 | ],
52 | "accounts": [
53 | {
54 | "name": "pool",
55 | "isMut": true,
56 | "isSigner": false,
57 | "docs": [
58 | "Liquidity Pool"
59 | ],
60 | "pda": {
61 | "seeds": [
62 | {
63 | "kind": "const",
64 | "type": "string",
65 | "value": "liquidity_pool"
66 | }
67 | ]
68 | }
69 | },
70 | {
71 | "name": "mint",
72 | "isMut": false,
73 | "isSigner": false,
74 | "docs": [
75 | "The mint account for the asset being deposited into the pool"
76 | ]
77 | },
78 | {
79 | "name": "poolTokenAccount",
80 | "isMut": true,
81 | "isSigner": false,
82 | "docs": [
83 | "The Liquidity Pool's token account for the asset being deposited into",
84 | "the pool"
85 | ]
86 | },
87 | {
88 | "name": "payerTokenAccount",
89 | "isMut": true,
90 | "isSigner": false,
91 | "docs": [
92 | "The payer's - or Liquidity Provider's - token account for the asset",
93 | "being deposited into the pool"
94 | ]
95 | },
96 | {
97 | "name": "payer",
98 | "isMut": true,
99 | "isSigner": true
100 | },
101 | {
102 | "name": "systemProgram",
103 | "isMut": false,
104 | "isSigner": false,
105 | "docs": [
106 | "System Program: Required for creating the Liquidity Pool's token account",
107 | "for the asset being deposited into the pool"
108 | ]
109 | },
110 | {
111 | "name": "tokenProgram",
112 | "isMut": false,
113 | "isSigner": false,
114 | "docs": [
115 | "Token Program: Required for transferring the assets from the Liquidity",
116 | "Provider's token account into the Liquidity Pool's token account"
117 | ]
118 | },
119 | {
120 | "name": "associatedTokenProgram",
121 | "isMut": false,
122 | "isSigner": false,
123 | "docs": [
124 | "Associated Token Program: Required for creating the Liquidity Pool's",
125 | "token account for the asset being deposited into the pool"
126 | ]
127 | }
128 | ],
129 | "args": [
130 | {
131 | "name": "amount",
132 | "type": "u64"
133 | }
134 | ]
135 | },
136 | {
137 | "name": "swap",
138 | "docs": [
139 | "Swap assets using the DEX"
140 | ],
141 | "accounts": [
142 | {
143 | "name": "pool",
144 | "isMut": true,
145 | "isSigner": false,
146 | "docs": [
147 | "Liquidity Pool"
148 | ],
149 | "pda": {
150 | "seeds": [
151 | {
152 | "kind": "const",
153 | "type": "string",
154 | "value": "liquidity_pool"
155 | }
156 | ]
157 | }
158 | },
159 | {
160 | "name": "receiveMint",
161 | "isMut": false,
162 | "isSigner": false,
163 | "docs": [
164 | "The mint account for the asset the user is requesting to receive in",
165 | "exchange"
166 | ]
167 | },
168 | {
169 | "name": "poolReceiveTokenAccount",
170 | "isMut": true,
171 | "isSigner": false,
172 | "docs": [
173 | "The Liquidity Pool's token account for the mint of the asset the user is",
174 | "requesting to receive in exchange (which will be debited)"
175 | ]
176 | },
177 | {
178 | "name": "payerReceiveTokenAccount",
179 | "isMut": true,
180 | "isSigner": false,
181 | "docs": [
182 | "The user's token account for the mint of the asset the user is",
183 | "requesting to receive in exchange (which will be credited)"
184 | ]
185 | },
186 | {
187 | "name": "payMint",
188 | "isMut": false,
189 | "isSigner": false,
190 | "docs": [
191 | "The mint account for the asset the user is proposing to pay in the swap"
192 | ]
193 | },
194 | {
195 | "name": "poolPayTokenAccount",
196 | "isMut": true,
197 | "isSigner": false,
198 | "docs": [
199 | "The Liquidity Pool's token account for the mint of the asset the user is",
200 | "proposing to pay in the swap (which will be credited)"
201 | ]
202 | },
203 | {
204 | "name": "payerPayTokenAccount",
205 | "isMut": true,
206 | "isSigner": false,
207 | "docs": [
208 | "The user's token account for the mint of the asset the user is",
209 | "proposing to pay in the swap (which will be debited)"
210 | ]
211 | },
212 | {
213 | "name": "payer",
214 | "isMut": true,
215 | "isSigner": true,
216 | "docs": [
217 | "The authority requesting to swap (user)"
218 | ]
219 | },
220 | {
221 | "name": "tokenProgram",
222 | "isMut": false,
223 | "isSigner": false,
224 | "docs": [
225 | "Token Program: Required for transferring the assets between all token",
226 | "accounts involved in the swap"
227 | ]
228 | },
229 | {
230 | "name": "systemProgram",
231 | "isMut": false,
232 | "isSigner": false
233 | },
234 | {
235 | "name": "associatedTokenProgram",
236 | "isMut": false,
237 | "isSigner": false
238 | }
239 | ],
240 | "args": [
241 | {
242 | "name": "amountToSwap",
243 | "type": "u64"
244 | }
245 | ]
246 | }
247 | ],
248 | "accounts": [
249 | {
250 | "name": "LiquidityPool",
251 | "docs": [
252 | "The `LiquidityPool` state - the inner data of the program-derived address",
253 | "that will be our Liquidity Pool"
254 | ],
255 | "type": {
256 | "kind": "struct",
257 | "fields": [
258 | {
259 | "name": "assets",
260 | "type": {
261 | "vec": "publicKey"
262 | }
263 | },
264 | {
265 | "name": "bump",
266 | "type": "u8"
267 | }
268 | ]
269 | }
270 | }
271 | ],
272 | "errors": [
273 | {
274 | "code": 6000,
275 | "name": "InvalidArithmetic",
276 | "msg": "Math overflow on `u64` value"
277 | },
278 | {
279 | "code": 6001,
280 | "name": "InvalidAssetKey",
281 | "msg": "An invalid asset mint address was provided"
282 | },
283 | {
284 | "code": 6002,
285 | "name": "InvalidSwapNotEnoughPay",
286 | "msg": "The amount proposed to pay is not great enough for at least 1 returned asset quantity"
287 | },
288 | {
289 | "code": 6003,
290 | "name": "InvalidSwapNotEnoughLiquidity",
291 | "msg": "The amount proposed to pay resolves to a receive amount that is greater than the current liquidity"
292 | },
293 | {
294 | "code": 6004,
295 | "name": "InvalidSwapMatchingAssets",
296 | "msg": "The asset proposed to pay is the same asset as the requested asset to receive"
297 | },
298 | {
299 | "code": 6005,
300 | "name": "InvalidSwapZeroAmount",
301 | "msg": "A user cannot propose to pay 0 of an asset"
302 | }
303 | ],
304 | "metadata": {
305 | "address": "4Nx62wVGSygnLwx3tf3q93JvkgjU5Tyx12ca3MTwxJnh"
306 | }
307 | }
--------------------------------------------------------------------------------
/app/src/idl/swap_program.ts:
--------------------------------------------------------------------------------
1 | export type SwapProgram = {
2 | "version": "0.1.0",
3 | "name": "swap_program",
4 | "instructions": [
5 | {
6 | "name": "createPool",
7 | "docs": [
8 | "Initialize the program by creating the liquidity pool"
9 | ],
10 | "accounts": [
11 | {
12 | "name": "pool",
13 | "isMut": true,
14 | "isSigner": false,
15 | "docs": [
16 | "Liquidity Pool"
17 | ],
18 | "pda": {
19 | "seeds": [
20 | {
21 | "kind": "const",
22 | "type": "string",
23 | "value": "liquidity_pool"
24 | }
25 | ]
26 | }
27 | },
28 | {
29 | "name": "payer",
30 | "isMut": true,
31 | "isSigner": true,
32 | "docs": [
33 | "Rent payer"
34 | ]
35 | },
36 | {
37 | "name": "systemProgram",
38 | "isMut": false,
39 | "isSigner": false,
40 | "docs": [
41 | "System Program: Required for creating the Liquidity Pool"
42 | ]
43 | }
44 | ],
45 | "args": []
46 | },
47 | {
48 | "name": "fundPool",
49 | "docs": [
50 | "Provide liquidity to the pool by funding it with some asset"
51 | ],
52 | "accounts": [
53 | {
54 | "name": "pool",
55 | "isMut": true,
56 | "isSigner": false,
57 | "docs": [
58 | "Liquidity Pool"
59 | ],
60 | "pda": {
61 | "seeds": [
62 | {
63 | "kind": "const",
64 | "type": "string",
65 | "value": "liquidity_pool"
66 | }
67 | ]
68 | }
69 | },
70 | {
71 | "name": "mint",
72 | "isMut": false,
73 | "isSigner": false,
74 | "docs": [
75 | "The mint account for the asset being deposited into the pool"
76 | ]
77 | },
78 | {
79 | "name": "poolTokenAccount",
80 | "isMut": true,
81 | "isSigner": false,
82 | "docs": [
83 | "The Liquidity Pool's token account for the asset being deposited into",
84 | "the pool"
85 | ]
86 | },
87 | {
88 | "name": "payerTokenAccount",
89 | "isMut": true,
90 | "isSigner": false,
91 | "docs": [
92 | "The payer's - or Liquidity Provider's - token account for the asset",
93 | "being deposited into the pool"
94 | ]
95 | },
96 | {
97 | "name": "payer",
98 | "isMut": true,
99 | "isSigner": true
100 | },
101 | {
102 | "name": "systemProgram",
103 | "isMut": false,
104 | "isSigner": false,
105 | "docs": [
106 | "System Program: Required for creating the Liquidity Pool's token account",
107 | "for the asset being deposited into the pool"
108 | ]
109 | },
110 | {
111 | "name": "tokenProgram",
112 | "isMut": false,
113 | "isSigner": false,
114 | "docs": [
115 | "Token Program: Required for transferring the assets from the Liquidity",
116 | "Provider's token account into the Liquidity Pool's token account"
117 | ]
118 | },
119 | {
120 | "name": "associatedTokenProgram",
121 | "isMut": false,
122 | "isSigner": false,
123 | "docs": [
124 | "Associated Token Program: Required for creating the Liquidity Pool's",
125 | "token account for the asset being deposited into the pool"
126 | ]
127 | }
128 | ],
129 | "args": [
130 | {
131 | "name": "amount",
132 | "type": "u64"
133 | }
134 | ]
135 | },
136 | {
137 | "name": "swap",
138 | "docs": [
139 | "Swap assets using the DEX"
140 | ],
141 | "accounts": [
142 | {
143 | "name": "pool",
144 | "isMut": true,
145 | "isSigner": false,
146 | "docs": [
147 | "Liquidity Pool"
148 | ],
149 | "pda": {
150 | "seeds": [
151 | {
152 | "kind": "const",
153 | "type": "string",
154 | "value": "liquidity_pool"
155 | }
156 | ]
157 | }
158 | },
159 | {
160 | "name": "receiveMint",
161 | "isMut": false,
162 | "isSigner": false,
163 | "docs": [
164 | "The mint account for the asset the user is requesting to receive in",
165 | "exchange"
166 | ]
167 | },
168 | {
169 | "name": "poolReceiveTokenAccount",
170 | "isMut": true,
171 | "isSigner": false,
172 | "docs": [
173 | "The Liquidity Pool's token account for the mint of the asset the user is",
174 | "requesting to receive in exchange (which will be debited)"
175 | ]
176 | },
177 | {
178 | "name": "payerReceiveTokenAccount",
179 | "isMut": true,
180 | "isSigner": false,
181 | "docs": [
182 | "The user's token account for the mint of the asset the user is",
183 | "requesting to receive in exchange (which will be credited)"
184 | ]
185 | },
186 | {
187 | "name": "payMint",
188 | "isMut": false,
189 | "isSigner": false,
190 | "docs": [
191 | "The mint account for the asset the user is proposing to pay in the swap"
192 | ]
193 | },
194 | {
195 | "name": "poolPayTokenAccount",
196 | "isMut": true,
197 | "isSigner": false,
198 | "docs": [
199 | "The Liquidity Pool's token account for the mint of the asset the user is",
200 | "proposing to pay in the swap (which will be credited)"
201 | ]
202 | },
203 | {
204 | "name": "payerPayTokenAccount",
205 | "isMut": true,
206 | "isSigner": false,
207 | "docs": [
208 | "The user's token account for the mint of the asset the user is",
209 | "proposing to pay in the swap (which will be debited)"
210 | ]
211 | },
212 | {
213 | "name": "payer",
214 | "isMut": true,
215 | "isSigner": true,
216 | "docs": [
217 | "The authority requesting to swap (user)"
218 | ]
219 | },
220 | {
221 | "name": "tokenProgram",
222 | "isMut": false,
223 | "isSigner": false,
224 | "docs": [
225 | "Token Program: Required for transferring the assets between all token",
226 | "accounts involved in the swap"
227 | ]
228 | },
229 | {
230 | "name": "systemProgram",
231 | "isMut": false,
232 | "isSigner": false
233 | },
234 | {
235 | "name": "associatedTokenProgram",
236 | "isMut": false,
237 | "isSigner": false
238 | }
239 | ],
240 | "args": [
241 | {
242 | "name": "amountToSwap",
243 | "type": "u64"
244 | }
245 | ]
246 | }
247 | ],
248 | "accounts": [
249 | {
250 | "name": "liquidityPool",
251 | "docs": [
252 | "The `LiquidityPool` state - the inner data of the program-derived address",
253 | "that will be our Liquidity Pool"
254 | ],
255 | "type": {
256 | "kind": "struct",
257 | "fields": [
258 | {
259 | "name": "assets",
260 | "type": {
261 | "vec": "publicKey"
262 | }
263 | },
264 | {
265 | "name": "bump",
266 | "type": "u8"
267 | }
268 | ]
269 | }
270 | }
271 | ],
272 | "errors": [
273 | {
274 | "code": 6000,
275 | "name": "InvalidArithmetic",
276 | "msg": "Math overflow on `u64` value"
277 | },
278 | {
279 | "code": 6001,
280 | "name": "InvalidAssetKey",
281 | "msg": "An invalid asset mint address was provided"
282 | },
283 | {
284 | "code": 6002,
285 | "name": "InvalidSwapNotEnoughPay",
286 | "msg": "The amount proposed to pay is not great enough for at least 1 returned asset quantity"
287 | },
288 | {
289 | "code": 6003,
290 | "name": "InvalidSwapNotEnoughLiquidity",
291 | "msg": "The amount proposed to pay resolves to a receive amount that is greater than the current liquidity"
292 | },
293 | {
294 | "code": 6004,
295 | "name": "InvalidSwapMatchingAssets",
296 | "msg": "The asset proposed to pay is the same asset as the requested asset to receive"
297 | },
298 | {
299 | "code": 6005,
300 | "name": "InvalidSwapZeroAmount",
301 | "msg": "A user cannot propose to pay 0 of an asset"
302 | }
303 | ]
304 | };
305 |
306 | export const IDL: SwapProgram = {
307 | "version": "0.1.0",
308 | "name": "swap_program",
309 | "instructions": [
310 | {
311 | "name": "createPool",
312 | "docs": [
313 | "Initialize the program by creating the liquidity pool"
314 | ],
315 | "accounts": [
316 | {
317 | "name": "pool",
318 | "isMut": true,
319 | "isSigner": false,
320 | "docs": [
321 | "Liquidity Pool"
322 | ],
323 | "pda": {
324 | "seeds": [
325 | {
326 | "kind": "const",
327 | "type": "string",
328 | "value": "liquidity_pool"
329 | }
330 | ]
331 | }
332 | },
333 | {
334 | "name": "payer",
335 | "isMut": true,
336 | "isSigner": true,
337 | "docs": [
338 | "Rent payer"
339 | ]
340 | },
341 | {
342 | "name": "systemProgram",
343 | "isMut": false,
344 | "isSigner": false,
345 | "docs": [
346 | "System Program: Required for creating the Liquidity Pool"
347 | ]
348 | }
349 | ],
350 | "args": []
351 | },
352 | {
353 | "name": "fundPool",
354 | "docs": [
355 | "Provide liquidity to the pool by funding it with some asset"
356 | ],
357 | "accounts": [
358 | {
359 | "name": "pool",
360 | "isMut": true,
361 | "isSigner": false,
362 | "docs": [
363 | "Liquidity Pool"
364 | ],
365 | "pda": {
366 | "seeds": [
367 | {
368 | "kind": "const",
369 | "type": "string",
370 | "value": "liquidity_pool"
371 | }
372 | ]
373 | }
374 | },
375 | {
376 | "name": "mint",
377 | "isMut": false,
378 | "isSigner": false,
379 | "docs": [
380 | "The mint account for the asset being deposited into the pool"
381 | ]
382 | },
383 | {
384 | "name": "poolTokenAccount",
385 | "isMut": true,
386 | "isSigner": false,
387 | "docs": [
388 | "The Liquidity Pool's token account for the asset being deposited into",
389 | "the pool"
390 | ]
391 | },
392 | {
393 | "name": "payerTokenAccount",
394 | "isMut": true,
395 | "isSigner": false,
396 | "docs": [
397 | "The payer's - or Liquidity Provider's - token account for the asset",
398 | "being deposited into the pool"
399 | ]
400 | },
401 | {
402 | "name": "payer",
403 | "isMut": true,
404 | "isSigner": true
405 | },
406 | {
407 | "name": "systemProgram",
408 | "isMut": false,
409 | "isSigner": false,
410 | "docs": [
411 | "System Program: Required for creating the Liquidity Pool's token account",
412 | "for the asset being deposited into the pool"
413 | ]
414 | },
415 | {
416 | "name": "tokenProgram",
417 | "isMut": false,
418 | "isSigner": false,
419 | "docs": [
420 | "Token Program: Required for transferring the assets from the Liquidity",
421 | "Provider's token account into the Liquidity Pool's token account"
422 | ]
423 | },
424 | {
425 | "name": "associatedTokenProgram",
426 | "isMut": false,
427 | "isSigner": false,
428 | "docs": [
429 | "Associated Token Program: Required for creating the Liquidity Pool's",
430 | "token account for the asset being deposited into the pool"
431 | ]
432 | }
433 | ],
434 | "args": [
435 | {
436 | "name": "amount",
437 | "type": "u64"
438 | }
439 | ]
440 | },
441 | {
442 | "name": "swap",
443 | "docs": [
444 | "Swap assets using the DEX"
445 | ],
446 | "accounts": [
447 | {
448 | "name": "pool",
449 | "isMut": true,
450 | "isSigner": false,
451 | "docs": [
452 | "Liquidity Pool"
453 | ],
454 | "pda": {
455 | "seeds": [
456 | {
457 | "kind": "const",
458 | "type": "string",
459 | "value": "liquidity_pool"
460 | }
461 | ]
462 | }
463 | },
464 | {
465 | "name": "receiveMint",
466 | "isMut": false,
467 | "isSigner": false,
468 | "docs": [
469 | "The mint account for the asset the user is requesting to receive in",
470 | "exchange"
471 | ]
472 | },
473 | {
474 | "name": "poolReceiveTokenAccount",
475 | "isMut": true,
476 | "isSigner": false,
477 | "docs": [
478 | "The Liquidity Pool's token account for the mint of the asset the user is",
479 | "requesting to receive in exchange (which will be debited)"
480 | ]
481 | },
482 | {
483 | "name": "payerReceiveTokenAccount",
484 | "isMut": true,
485 | "isSigner": false,
486 | "docs": [
487 | "The user's token account for the mint of the asset the user is",
488 | "requesting to receive in exchange (which will be credited)"
489 | ]
490 | },
491 | {
492 | "name": "payMint",
493 | "isMut": false,
494 | "isSigner": false,
495 | "docs": [
496 | "The mint account for the asset the user is proposing to pay in the swap"
497 | ]
498 | },
499 | {
500 | "name": "poolPayTokenAccount",
501 | "isMut": true,
502 | "isSigner": false,
503 | "docs": [
504 | "The Liquidity Pool's token account for the mint of the asset the user is",
505 | "proposing to pay in the swap (which will be credited)"
506 | ]
507 | },
508 | {
509 | "name": "payerPayTokenAccount",
510 | "isMut": true,
511 | "isSigner": false,
512 | "docs": [
513 | "The user's token account for the mint of the asset the user is",
514 | "proposing to pay in the swap (which will be debited)"
515 | ]
516 | },
517 | {
518 | "name": "payer",
519 | "isMut": true,
520 | "isSigner": true,
521 | "docs": [
522 | "The authority requesting to swap (user)"
523 | ]
524 | },
525 | {
526 | "name": "tokenProgram",
527 | "isMut": false,
528 | "isSigner": false,
529 | "docs": [
530 | "Token Program: Required for transferring the assets between all token",
531 | "accounts involved in the swap"
532 | ]
533 | },
534 | {
535 | "name": "systemProgram",
536 | "isMut": false,
537 | "isSigner": false
538 | },
539 | {
540 | "name": "associatedTokenProgram",
541 | "isMut": false,
542 | "isSigner": false
543 | }
544 | ],
545 | "args": [
546 | {
547 | "name": "amountToSwap",
548 | "type": "u64"
549 | }
550 | ]
551 | }
552 | ],
553 | "accounts": [
554 | {
555 | "name": "liquidityPool",
556 | "docs": [
557 | "The `LiquidityPool` state - the inner data of the program-derived address",
558 | "that will be our Liquidity Pool"
559 | ],
560 | "type": {
561 | "kind": "struct",
562 | "fields": [
563 | {
564 | "name": "assets",
565 | "type": {
566 | "vec": "publicKey"
567 | }
568 | },
569 | {
570 | "name": "bump",
571 | "type": "u8"
572 | }
573 | ]
574 | }
575 | }
576 | ],
577 | "errors": [
578 | {
579 | "code": 6000,
580 | "name": "InvalidArithmetic",
581 | "msg": "Math overflow on `u64` value"
582 | },
583 | {
584 | "code": 6001,
585 | "name": "InvalidAssetKey",
586 | "msg": "An invalid asset mint address was provided"
587 | },
588 | {
589 | "code": 6002,
590 | "name": "InvalidSwapNotEnoughPay",
591 | "msg": "The amount proposed to pay is not great enough for at least 1 returned asset quantity"
592 | },
593 | {
594 | "code": 6003,
595 | "name": "InvalidSwapNotEnoughLiquidity",
596 | "msg": "The amount proposed to pay resolves to a receive amount that is greater than the current liquidity"
597 | },
598 | {
599 | "code": 6004,
600 | "name": "InvalidSwapMatchingAssets",
601 | "msg": "The asset proposed to pay is the same asset as the requested asset to receive"
602 | },
603 | {
604 | "code": 6005,
605 | "name": "InvalidSwapZeroAmount",
606 | "msg": "A user cannot propose to pay 0 of an asset"
607 | }
608 | ]
609 | };
610 |
--------------------------------------------------------------------------------
/app/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { AppProps } from 'next/app'
2 | import Head from 'next/head'
3 | import { FC } from 'react'
4 | import { AppBar } from '@/components/AppBar'
5 | import { ContentContainer } from '@/components/ContentContainer'
6 | import Notifications from '@/components/Notification'
7 | import { ContextProvider } from '@/contexts/ContextProvider'
8 | require('@solana/wallet-adapter-react-ui/styles.css')
9 | require('../styles/globals.css')
10 |
11 | const App: FC = ({ Component, pageProps }) => {
12 | return (
13 | <>
14 |
15 | Arbitrage Pirate
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | >
28 | )
29 | }
30 |
31 | export default App
32 |
--------------------------------------------------------------------------------
/app/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next'
3 |
4 | type Data = {
5 | name: string
6 | }
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse
11 | ) {
12 | res.status(200).json({ name: 'John Doe' })
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import { HomeView } from '@/views'
3 |
4 | export default function Home() {
5 | return (
6 | <>
7 |
8 | Arbitrage Pirate
9 |
13 |
17 |
18 |
19 |
20 | >
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/stores/useAssetsStore.tsx:
--------------------------------------------------------------------------------
1 | import { Program } from '@coral-xyz/anchor'
2 | import {
3 | PROGRAM_ID as METADATA_PROGRAM_ID,
4 | Metadata,
5 | } from '@metaplex-foundation/mpl-token-metadata'
6 | import { PublicKey } from '@solana/web3.js'
7 | import { SwapProgram } from '@/idl/swap_program'
8 | import {
9 | getAssociatedTokenAddressSync,
10 | getMultipleAccounts as getMultipleTokenAccounts,
11 | } from '@solana/spl-token'
12 |
13 | // Seed prefix for the Liquidity Pool from our program
14 | const LIQUIDITY_POOL_SEED_PREFIX = 'liquidity_pool'
15 |
16 | const getPoolAddress = (programId: PublicKey) =>
17 | PublicKey.findProgramAddressSync(
18 | [Buffer.from(LIQUIDITY_POOL_SEED_PREFIX)],
19 | programId
20 | )[0]
21 |
22 | const getMetadataAddress = (programId: PublicKey, mint: PublicKey) =>
23 | PublicKey.findProgramAddressSync(
24 | [
25 | Buffer.from('metadata'),
26 | METADATA_PROGRAM_ID.toBuffer(),
27 | mint.toBuffer(),
28 | ],
29 | METADATA_PROGRAM_ID
30 | )[0]
31 |
32 | export interface Asset {
33 | name: string
34 | symbol: string
35 | uri: string
36 | decimals: number
37 | balance: number
38 | mint: PublicKey
39 | poolTokenAccount: PublicKey
40 | }
41 |
42 | export const getAssets = async (
43 | program: Program
44 | ): Promise => {
45 | let assets: Asset[]
46 | const poolAddress = getPoolAddress(program.programId)
47 | const pool = await program.account.liquidityPool.fetch(poolAddress)
48 | let metadataAddresses: PublicKey[] = []
49 | let tokenAccountAddresses: PublicKey[] = []
50 | let mintAddresses: PublicKey[] = []
51 | pool.assets.forEach((m) => {
52 | metadataAddresses.push(getMetadataAddress(program.programId, m))
53 | tokenAccountAddresses.push(
54 | getAssociatedTokenAddressSync(m, poolAddress, true)
55 | )
56 | mintAddresses.push(m) // assuming pool.assets are the mint addresses
57 | })
58 | const poolTokenAccounts = await getMultipleTokenAccounts(
59 | program.provider.connection,
60 | tokenAccountAddresses
61 | )
62 |
63 | const metadataAccounts = (
64 | await program.provider.connection.getMultipleAccountsInfo(
65 | metadataAddresses
66 | )
67 | ).map((accountInfo) =>
68 | accountInfo != null ? Metadata.deserialize(accountInfo?.data) : null
69 | )
70 |
71 | const mintInfos = await Promise.all(
72 | mintAddresses.map((mint) =>
73 | program.provider.connection.getParsedAccountInfo(mint)
74 | )
75 | )
76 |
77 | assets = poolTokenAccounts.map((account, index) => {
78 | const metadataAccount = metadataAccounts.find((m) =>
79 | m?.[0].mint.equals(account.mint)
80 | )
81 | const [name, symbol, uri] = metadataAccount
82 | ? [
83 | metadataAccount[0].data.name,
84 | metadataAccount[0].data.symbol,
85 | metadataAccount[0].data.uri,
86 | ]
87 | : ['Unknown Asset', 'UNKN', '']
88 | let decimals = 0
89 | // @ts-ignore
90 | if ('parsed' in mintInfos[index].value.data) {
91 | // @ts-ignore
92 | decimals = mintInfos[index].value.data.parsed.info.decimals
93 | }
94 | return {
95 | name,
96 | symbol,
97 | uri,
98 | decimals,
99 | balance: Number(account.amount),
100 | mint: account.mint,
101 | poolTokenAccount: account.address,
102 | }
103 | })
104 | return assets
105 | }
106 |
--------------------------------------------------------------------------------
/app/src/stores/useNotificationStore.tsx:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 | import { produce, Draft } from 'immer'
3 |
4 | interface NotificationStore {
5 | notifications: Array<{
6 | type: string
7 | message: string
8 | description?: string
9 | txid?: string
10 | }>
11 | set: (fn: (draft: Draft) => void) => void
12 | }
13 |
14 | const useNotificationStore = create((set) => ({
15 | notifications: [],
16 | set: (fn) =>
17 | set(
18 | produce((draft) => {
19 | fn(draft)
20 | })
21 | ),
22 | }))
23 |
24 | export default useNotificationStore
25 |
--------------------------------------------------------------------------------
/app/src/stores/useUserSOLBalanceStore.tsx:
--------------------------------------------------------------------------------
1 | import { Connection, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
2 | import { create } from 'zustand';
3 |
4 | interface UserSOLBalanceStore {
5 | balanceSol: number;
6 | getUserSOLBalance: (publicKey: PublicKey, connection: Connection) => void;
7 | }
8 |
9 | const useUserSOLBalanceStore = create((set, _get) => ({
10 | balanceSol: 0,
11 | getUserSOLBalance: async (publicKey, connection) => {
12 | let balanceSol = 0;
13 | try {
14 | balanceSol = await connection.getBalance(publicKey, 'confirmed');
15 | balanceSol = balanceSol / LAMPORTS_PER_SOL;
16 | } catch (e) {
17 | console.log(`error getting balanceSol: `, e);
18 | }
19 | set({
20 | balanceSol,
21 | });
22 | },
23 | }));
24 |
25 | export default useUserSOLBalanceStore;
26 |
--------------------------------------------------------------------------------
/app/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/app/src/utils/const.ts:
--------------------------------------------------------------------------------
1 | import { PublicKey } from '@solana/web3.js';
2 |
3 | export const SOL_USD_PRICE_FEED_ID = new PublicKey(
4 | 'ALP8SdU9oARYVLgLR7LrqMNCYBnhtnQz1cj6bwgwQmgj',
5 | );
6 | export const USDC_MINT = new PublicKey(
7 | '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU',
8 | );
9 |
10 | export const LOAN_ESCROW_SEED_PREFIX = 'loan_escrow';
11 | export const LOAN_NOTE_MINT_SEED_PREFIX = 'loan_note_mint';
12 |
13 | export const REQUIRED_FLOAT_PERCENTAGE = 75;
14 |
15 | export type LoanStatus = 'available' | 'claimed' | 'closed';
16 |
--------------------------------------------------------------------------------
/app/src/utils/explorer.ts:
--------------------------------------------------------------------------------
1 | import { PublicKey, Transaction } from '@solana/web3.js'
2 | import base58 from 'bs58'
3 |
4 | export function getExplorerUrl(
5 | endpoint: string,
6 | viewTypeOrItemAddress: 'inspector' | PublicKey | string,
7 | itemType = 'address' // | 'tx' | 'block'
8 | ) {
9 | const getClusterUrlParam = () => {
10 | let cluster = ''
11 | if (endpoint === 'localnet') {
12 | cluster = `custom&customUrl=${encodeURIComponent(
13 | 'http://127.0.0.1:8899'
14 | )}`
15 | } else if (endpoint === 'https://api.devnet.solana.com') {
16 | cluster = 'devnet'
17 | }
18 |
19 | return cluster ? `?cluster=${cluster}` : ''
20 | }
21 |
22 | return `https://explorer.solana.com/${itemType}/${viewTypeOrItemAddress}${getClusterUrlParam()}`
23 | }
--------------------------------------------------------------------------------
/app/src/utils/index.tsx:
--------------------------------------------------------------------------------
1 | import { format } from 'date-fns'
2 |
3 | // Concatenates classes into a single className string
4 | const cn = (...args: string[]) => args.join(' ')
5 |
6 | const formatDate = (date: string) =>
7 | format(new Date(date), 'MM/dd/yyyy h:mm:ss')
8 |
9 | /**
10 | * Formats number as currency string.
11 | *
12 | * @param number Number to format.
13 | */
14 | const numberToCurrencyString = (number: number) =>
15 | number.toLocaleString('en-US')
16 |
17 | /**
18 | * Returns a number whose value is limited to the given range.
19 | *
20 | * Example: limit the output of this computation to between 0 and 255
21 | * (x * 255).clamp(0, 255)
22 | *
23 | * @param {Number} min The lower boundary of the output range
24 | * @param {Number} max The upper boundary of the output range
25 | * @returns A number in the range [min, max]
26 | * @type Number
27 | */
28 | const clamp = (current: number, min: number, max: number) =>
29 | Math.min(Math.max(current, min), max)
30 |
31 | export { cn, formatDate, numberToCurrencyString, clamp }
32 |
--------------------------------------------------------------------------------
/app/src/utils/notifications.tsx:
--------------------------------------------------------------------------------
1 | import useNotificationStore from "@/stores/useNotificationStore";
2 |
3 | export function notify(newNotification: {
4 | type?: string
5 | message: string
6 | description?: string
7 | txid?: string
8 | }) {
9 | const {
10 | notifications,
11 | set: setNotificationStore,
12 | } = useNotificationStore.getState()
13 |
14 | setNotificationStore((state: { notifications: any[] }) => {
15 | state.notifications = [
16 | ...notifications,
17 | { type: 'success', ...newNotification },
18 | ]
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/utils/program.ts:
--------------------------------------------------------------------------------
1 | import * as borsh from 'borsh'
2 | import { Buffer } from 'buffer'
3 | import {
4 | AccountMeta,
5 | AddressLookupTableAccount,
6 | Connection,
7 | Keypair,
8 | PublicKey,
9 | SystemProgram,
10 | SYSVAR_RENT_PUBKEY,
11 | TransactionInstruction,
12 | TransactionMessage,
13 | VersionedTransaction,
14 | } from '@solana/web3.js'
15 | import { TOKEN_PROGRAM_ID } from '@solana/spl-token'
16 |
17 | /**
18 | * The address of the Arbitrage Program
19 | */
20 | export const ARBITRAGE_PROGRAM = new PublicKey('')
21 |
22 | /**
23 | * The address of the Arbitrage Program's Lookup Table
24 | */
25 | export const ARBITRAGE_LOOKUP_TABLE = new PublicKey('')
26 |
27 | /**
28 | * Get the PDA of the Liquidity Pool for a program
29 | */
30 | export function getPoolAddress(programId: PublicKey): PublicKey {
31 | return PublicKey.findProgramAddressSync(
32 | [Buffer.from('liquidity_pool')],
33 | programId
34 | )[0]
35 | }
36 |
37 | /**
38 | * Arbitrage program instructions
39 | */
40 | class ArbitrageProgramInstruction {
41 | instruction: number
42 | swap_1_program_id: Uint8Array
43 | swap_2_program_id: Uint8Array
44 | concurrency: number
45 | temperature: number
46 | constructor(props: {
47 | swapProgram1: PublicKey
48 | swapProgram2: PublicKey
49 | concurrency: number
50 | temperature: number
51 | }) {
52 | this.instruction = 0
53 | this.swap_1_program_id = props.swapProgram1.toBuffer()
54 | this.swap_2_program_id = props.swapProgram2.toBuffer()
55 | this.concurrency = props.concurrency
56 | this.temperature = props.temperature
57 | }
58 | toBuffer() {
59 | return Buffer.from(
60 | borsh.serialize(ArbitrageProgramInstructionSchema, this)
61 | )
62 | }
63 | }
64 |
65 | const ArbitrageProgramInstructionSchema = new Map([
66 | [
67 | ArbitrageProgramInstruction,
68 | {
69 | kind: 'struct',
70 | fields: [
71 | ['instruction', 'u8'],
72 | ['swap_1_program_id', [32]],
73 | ['swap_2_program_id', [32]],
74 | ['concurrency', 'u8'],
75 | ['temperature', 'u8'],
76 | ],
77 | },
78 | ],
79 | ])
80 |
81 | /**
82 | *
83 | * "Default" `AccountMeta` (marks as mutable non-signer)
84 | * Used for Token Accounts to "lock" them on the Sealevel runtime
85 | *
86 | * @param pubkey Address of the account
87 | * @returns `KeyArg`
88 | */
89 | export function defaultAccountMeta(pubkey: PublicKey): AccountMeta {
90 | return { pubkey, isSigner: false, isWritable: true }
91 | }
92 |
93 | /**
94 | *
95 | * Creates the instruction for our Arbitrage Program
96 | *
97 | * @param programId Arbitrage program ID
98 | * @param payer Token payer (the one funding the arb)
99 | * @param tokenAccountsUser The payer's token accounts
100 | * @param tokenAccountsSwap1 Swap #1's token accounts
101 | * @param tokenAccountsSwap2 Swap #2's token accounts
102 | * @param mints The asset mints
103 | * @param concurrency How many accounts we're evaluating at once
104 | * @param swapProgram1 Swap #1 program ID
105 | * @param swapProgram2 Swap #2 program ID
106 | * @returns `TransactionInstruction`
107 | */
108 | export function createArbitrageInstruction(
109 | payer: PublicKey,
110 | tokenAccountsUser: PublicKey[],
111 | tokenAccountsSwap1: PublicKey[],
112 | tokenAccountsSwap2: PublicKey[],
113 | mints: PublicKey[],
114 | concurrency: number,
115 | temperature: number,
116 | swapProgram1: PublicKey,
117 | swapProgram2: PublicKey
118 | ): TransactionInstruction {
119 | let swapPool1 = getPoolAddress(swapProgram1)
120 | let swapPool2 = getPoolAddress(swapProgram2)
121 | const data = new ArbitrageProgramInstruction({
122 | swapProgram1,
123 | swapProgram2,
124 | concurrency,
125 | temperature,
126 | }).toBuffer()
127 | let keys: AccountMeta[] = [
128 | // Payer
129 | { pubkey: payer, isSigner: true, isWritable: true },
130 | // Token Program
131 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
132 | // Liquidity Pool for Swap #1
133 | defaultAccountMeta(swapPool1),
134 | // Liquidity Pool for Swap #2
135 | defaultAccountMeta(swapPool2),
136 | ]
137 | // [Token Accounts for User]
138 | tokenAccountsUser.forEach((a) => keys.push(defaultAccountMeta(a)))
139 | // [Token Accounts for Swap #1]
140 | tokenAccountsSwap1.forEach((a) => keys.push(defaultAccountMeta(a)))
141 | // [Token Accounts for Swap #2]
142 | tokenAccountsSwap2.forEach((a) => keys.push(defaultAccountMeta(a)))
143 | // [Mint Accounts]
144 | mints.forEach((a) => keys.push(defaultAccountMeta(a)))
145 | return new TransactionInstruction({
146 | keys,
147 | programId: ARBITRAGE_PROGRAM,
148 | data,
149 | })
150 | }
151 |
152 | /**
153 | *
154 | * Get an Address Lookup Table account
155 | *
156 | * @param connection Connection to Solana RPC
157 | * @param lookupTablePubkey The address of the Address Lookup Table
158 | */
159 | export async function getAddressLookupTable(
160 | connection: Connection,
161 | lookupTablePubkey: PublicKey
162 | ): Promise {
163 | return connection
164 | .getAddressLookupTable(lookupTablePubkey)
165 | .then((res) => res.value)
166 | }
167 |
168 | /**
169 | *
170 | * Builds a transaction using the V0 format
171 | * using an Address Lookup Table
172 | *
173 | * @param connection Connection to Solana RPC
174 | * @param instructions Instructions to send
175 | * @param payer Transaction Fee Payer
176 | * @param signers All required signers, in order
177 | * @param lookupTablePubkey The address of the Address Lookup Table to use
178 | * @returns The transaction v0
179 | */
180 | export async function buildTransactionV0WithLookupTable(
181 | connection: Connection,
182 | instructions: TransactionInstruction[],
183 | payer: PublicKey,
184 | signers: Keypair[]
185 | ): Promise {
186 | const lookupTableAccount = await getAddressLookupTable(
187 | connection,
188 | ARBITRAGE_LOOKUP_TABLE
189 | )
190 | if (lookupTableAccount == null) {
191 | throw `Lookup Table not found for ${ARBITRAGE_LOOKUP_TABLE.toBase58()}`
192 | }
193 | let blockhash = await connection
194 | .getLatestBlockhash()
195 | .then((res) => res.blockhash)
196 | // Compile V0 Message with the Lookup Table
197 | const messageV0 = new TransactionMessage({
198 | payerKey: payer,
199 | recentBlockhash: blockhash,
200 | instructions,
201 | }).compileToV0Message([lookupTableAccount])
202 | const tx = new VersionedTransaction(messageV0)
203 | signers.forEach((s) => tx.sign([s]))
204 | return tx
205 | }
206 |
--------------------------------------------------------------------------------
/app/src/views/home/index.tsx:
--------------------------------------------------------------------------------
1 | import { useWallet } from '@solana/wallet-adapter-react'
2 | import ArbCard from '@/components/ArbCard'
3 | import { FC } from 'react'
4 | import Image from 'next/image'
5 |
6 | export const HomeView: FC = () => {
7 | const wallet = useWallet()
8 |
9 | return (
10 |
11 | {wallet.connected ? (
12 |
15 | ) : (
16 |
17 |
18 | Connect a wallet to start pillaging some ports
19 |
20 |
21 | )}
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/views/index.tsx:
--------------------------------------------------------------------------------
1 | export { HomeView } from './home';
2 |
--------------------------------------------------------------------------------
/app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | mode: 'jit',
4 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
5 | darkMode: 'media',
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [require('daisyui'), require('@tailwindcss/typography')],
10 | daisyui: {
11 | styled: true,
12 | // TODO: Theme needs works
13 | themes: [
14 | {
15 | solana: {
16 | fontFamily: {
17 | display: ['PT Mono, monospace'],
18 | body: ['Inter, sans-serif'],
19 | },
20 | primary: '#000000' /* Primary color */,
21 | 'primary-focus': '#9945FF' /* Primary color - focused */,
22 | 'primary-content':
23 | '#ffffff' /* Foreground content color to use on primary color */,
24 |
25 | secondary: '#808080' /* Secondary color */,
26 | 'secondary-focus':
27 | '#f3cc30' /* Secondary color - focused */,
28 | 'secondary-content':
29 | '#ffffff' /* Foreground content color to use on secondary color */,
30 |
31 | accent: '#33a382' /* Accent color */,
32 | 'accent-focus': '#2aa79b' /* Accent color - focused */,
33 | 'accent-content':
34 | '#ffffff' /* Foreground content color to use on accent color */,
35 |
36 | neutral: '#2b2b2b' /* Neutral color */,
37 | 'neutral-focus': '#2a2e37' /* Neutral color - focused */,
38 | 'neutral-content':
39 | '#ffffff' /* Foreground content color to use on neutral color */,
40 |
41 | 'base-100':
42 | '#000000' /* Base color of page, used for blank backgrounds */,
43 | 'base-200': '#35363a' /* Base color, a little darker */,
44 | 'base-300': '#222222' /* Base color, even more darker */,
45 | 'base-content':
46 | '#f9fafb' /* Foreground content color to use on base color */,
47 |
48 | info: '#2094f3' /* Info */,
49 | success: '#009485' /* Success */,
50 | warning: '#ff9900' /* Warning */,
51 | error: '#ff5724' /* Error */,
52 | },
53 | },
54 | // backup themes:
55 | // 'dark',
56 | // 'synthwave'
57 | ],
58 | base: true,
59 | utils: true,
60 | logs: true,
61 | rtl: false,
62 | },
63 | }
64 |
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "paths": {
18 | "@/*": ["./src/*"]
19 | }
20 | },
21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check",
5 | "test": "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/main.test.ts"
6 | },
7 | "dependencies": {
8 | "@solana/spl-token": "^0.3.7",
9 | "@solana/web3.js": "^1.75.0"
10 | },
11 | "devDependencies": {
12 | "@types/bn.js": "^5.1.0",
13 | "@types/chai": "^4.3.0",
14 | "@types/mocha": "^9.0.0",
15 | "chai": "^4.3.4",
16 | "mocha": "^9.0.3",
17 | "prettier": "^2.6.2",
18 | "ts-mocha": "^10.0.0",
19 | "typescript": "^4.3.5"
20 | }
21 | }
--------------------------------------------------------------------------------
/program/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "arb-program"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | crate-type = ["cdylib", "lib"]
8 | name = "arb_program"
9 |
10 | [dependencies]
11 | borsh = "=0.9.3"
12 | borsh-derive = "=0.9.3"
13 | bytemuck = "1.7.2"
14 | getrandom = { version = "0.2.2", features = ["custom"] }
15 | num-derive = "0.3.3"
16 | num-traits = "0.2.15"
17 | solana-program = "=1.14.18"
18 | spl-associated-token-account = { version = "1.1.3", features = ["no-entrypoint"] }
19 | spl-token = { version = "3.5.0", features = ["no-entrypoint"] }
20 | spl-token-2022 = { version = "0.6.1", features = ["no-entrypoint"] }
21 | thiserror = "1.0.40"
--------------------------------------------------------------------------------
/program/src/arb.rs:
--------------------------------------------------------------------------------
1 | //! Arbitrage opportunity spotting and trade placement
2 | use solana_program::{
3 | account_info::AccountInfo, entrypoint::ProgramResult, instruction::Instruction, msg,
4 | program::invoke, pubkey::Pubkey,
5 | };
6 |
7 | use crate::{
8 | error::ArbitrageProgramError,
9 | partial_state::{ArbitrageMintInfo, ArbitrageTokenAccountInfo},
10 | swap::determine_swap_receive,
11 | util::{ArbitrageEvaluateOption, ToAccountMeta},
12 | };
13 |
14 | /// Args for the `try_arbitrage` algorithm
15 | pub struct TryArbitrageArgs<'a, 'b> {
16 | pub token_accounts_user: Vec>,
17 | pub token_accounts_swap_1: Vec>,
18 | pub token_accounts_swap_2: Vec>,
19 | pub mints: Vec>,
20 | pub payer: &'a AccountInfo<'b>,
21 | pub token_program: &'a AccountInfo<'b>,
22 | pub system_program: &'a AccountInfo<'b>,
23 | pub associated_token_program: &'a AccountInfo<'b>,
24 | pub swap_1_program: &'a AccountInfo<'b>,
25 | pub swap_2_program: &'a AccountInfo<'b>,
26 | pub swap_1_pool: &'a AccountInfo<'b>,
27 | pub swap_2_pool: &'a AccountInfo<'b>,
28 | pub temperature: u8,
29 | }
30 |
31 | /// Checks to see if there is an arbitrage opportunity between the two pools,
32 | /// and executes the trade if there is one
33 | pub fn try_arbitrage(args: TryArbitrageArgs<'_, '_>) -> ProgramResult {
34 | msg!("Swap #1 Pool: {}", args.swap_1_pool.key);
35 | msg!("Swap #2 Pool: {}", args.swap_2_pool.key);
36 | let mints_len = args.mints.len();
37 | for i in 0..mints_len {
38 | // Load each token account and the mint for the asset we want to drive arbitrage
39 | // with
40 | let user_i = args.token_accounts_user.get(i).ok_or_arb_err()?;
41 | let swap_1_i = args.token_accounts_swap_1.get(i).ok_or_arb_err()?;
42 | let swap_2_i = args.token_accounts_swap_2.get(i).ok_or_arb_err()?;
43 | let mint_i = args.mints.get(i).ok_or_arb_err()?;
44 | for j in (i + 1)..mints_len {
45 | // Load each token account and the mint for the asset we are investigating
46 | // arbitrage trading against
47 | let user_j = args.token_accounts_user.get(j).ok_or_arb_err()?;
48 | let swap_1_j = args.token_accounts_swap_1.get(j).ok_or_arb_err()?;
49 | let swap_2_j = args.token_accounts_swap_2.get(j).ok_or_arb_err()?;
50 | let mint_j = args.mints.get(j).ok_or_arb_err()?;
51 | // Calculate how much of each asset we can expect to receive for our proposed
52 | // asset we would pay
53 | let r_swap_1 =
54 | determine_swap_receive(swap_1_j.3, mint_j.1, swap_1_i.3, mint_i.1, user_i.3)?;
55 | let r_swap_2 =
56 | determine_swap_receive(swap_2_j.3, mint_j.1, swap_2_i.3, mint_i.1, user_i.3)?;
57 | if r_swap_1 == 0 || r_swap_1 > swap_1_j.3 || r_swap_2 == 0 || r_swap_2 > swap_2_j.3 {
58 | continue;
59 | }
60 | // Evaluate the arbitrage check
61 | if let Some(trade) = check_for_arbitrage(r_swap_1, r_swap_2, args.temperature) {
62 | // If we have a trade, place it
63 | msg!("PLACING TRADE!");
64 | return match trade {
65 | // Buy on Swap #1 and sell on Swap #2
66 | Buy::Swap1 => {
67 | msg!("Buy on Swap #1 and sell on Swap #2");
68 | invoke_arbitrage(
69 | (
70 | *args.swap_1_program.key,
71 | &[
72 | args.swap_1_pool.to_owned(),
73 | mint_j.0.to_owned(),
74 | swap_1_j.0.to_owned(),
75 | user_j.0.to_owned(),
76 | mint_i.0.to_owned(),
77 | swap_1_i.0.to_owned(),
78 | user_i.0.to_owned(),
79 | args.payer.to_owned(),
80 | args.token_program.to_owned(),
81 | args.system_program.to_owned(),
82 | args.associated_token_program.to_owned(),
83 | ],
84 | user_i.3,
85 | ),
86 | (
87 | *args.swap_2_program.key,
88 | &[
89 | args.swap_2_pool.to_owned(),
90 | mint_i.0.to_owned(),
91 | swap_2_i.0.to_owned(),
92 | user_i.0.to_owned(),
93 | mint_j.0.to_owned(),
94 | swap_2_j.0.to_owned(),
95 | user_j.0.to_owned(),
96 | args.payer.to_owned(),
97 | args.token_program.to_owned(),
98 | args.system_program.to_owned(),
99 | args.associated_token_program.to_owned(),
100 | ],
101 | r_swap_1,
102 | ),
103 | )
104 | }
105 | // Buy on Swap #2 and sell on Swap #1
106 | Buy::Swap2 => {
107 | msg!("Buy on Swap #2 and sell on Swap #1");
108 | invoke_arbitrage(
109 | (
110 | *args.swap_2_program.key,
111 | &[
112 | args.swap_2_pool.to_owned(),
113 | mint_j.0.to_owned(),
114 | swap_2_j.0.to_owned(),
115 | user_j.0.to_owned(),
116 | mint_i.0.to_owned(),
117 | swap_2_i.0.to_owned(),
118 | user_i.0.to_owned(),
119 | args.payer.to_owned(),
120 | args.token_program.to_owned(),
121 | args.system_program.to_owned(),
122 | args.associated_token_program.to_owned(),
123 | ],
124 | r_swap_2,
125 | ),
126 | (
127 | *args.swap_1_program.key,
128 | &[
129 | args.swap_1_pool.to_owned(),
130 | mint_i.0.to_owned(),
131 | swap_1_i.0.to_owned(),
132 | user_i.0.to_owned(),
133 | mint_j.0.to_owned(),
134 | swap_1_j.0.to_owned(),
135 | user_j.0.to_owned(),
136 | args.payer.to_owned(),
137 | args.token_program.to_owned(),
138 | args.system_program.to_owned(),
139 | args.associated_token_program.to_owned(),
140 | ],
141 | user_j.3,
142 | ),
143 | )
144 | }
145 | };
146 | }
147 | }
148 | }
149 | Err(ArbitrageProgramError::NoArbitrage.into())
150 | }
151 |
152 | /// Enum used to tell the algorithm which swap pool is a "buy"
153 | enum Buy {
154 | /// Buy on Swap #1 and sell on Swap #2
155 | Swap1,
156 | /// Buy on Swap #2 and sell on Swap #1
157 | Swap2,
158 | }
159 |
160 | /// Evaluates the percent difference in the calculated values for `r` and
161 | /// determines which pool to buy or sell, if any
162 | fn check_for_arbitrage(r_swap_1: u64, r_swap_2: u64, temperature: u8) -> Option {
163 | // Calculate our appetite for tighter differences in `r` values based on the
164 | // provided `temperature`
165 | let threshold = 100.0 - temperature as f64;
166 | // Calculate the percent difference of `r` for Swap #1 vs. `r` for Swap #2
167 | let percent_diff = (r_swap_1 as i64 - r_swap_2 as i64) as f64 / r_swap_2 as f64 * 100.0;
168 | if percent_diff.abs() > threshold {
169 | if percent_diff > 0.0 {
170 | // If `r` for Swap #1 is greater than `r` for Swap #2, that means we want to buy
171 | // from Swap #1
172 | Some(Buy::Swap1)
173 | } else {
174 | // If `r` for Swap #2 is greater than `r` for Swap #1, that means we want to buy
175 | // from Swap #2
176 | Some(Buy::Swap2)
177 | }
178 | } else {
179 | None
180 | }
181 | }
182 |
183 | /// Invokes the arbitrage trade by sending a cross-program invocation (CPI)
184 | /// first to the swap program we intend to buy from (receive), and then
185 | /// immediately send another CPI to the swap program we intend to sell to
186 | fn invoke_arbitrage(
187 | buy: (Pubkey, &[AccountInfo], u64),
188 | sell: (Pubkey, &[AccountInfo], u64),
189 | ) -> ProgramResult {
190 | let (buy_swap_ix_data, sell_swap_ix_data) = build_ix_datas(buy.2, sell.2);
191 | let ix_buy = Instruction::new_with_borsh(
192 | buy.0,
193 | &buy_swap_ix_data,
194 | buy.1.iter().map(ToAccountMeta::to_account_meta).collect(),
195 | );
196 | let ix_sell = Instruction::new_with_borsh(
197 | sell.0,
198 | &sell_swap_ix_data,
199 | sell.1.iter().map(ToAccountMeta::to_account_meta).collect(),
200 | );
201 | msg!("Executing buy ...");
202 | invoke(&ix_buy, buy.1)?;
203 | msg!("Executing sell ...");
204 | invoke(&ix_sell, sell.1)?;
205 | Ok(())
206 | }
207 |
208 | /// Used to build the instruction data for the `swap` instruction
209 | /// on each swap program
210 | fn build_ix_datas(buy_amount: u64, sell_amount: u64) -> ([u8; 16], [u8; 16]) {
211 | // Initialize both datas
212 | let mut buy_swap_ix_data = [0u8; 16];
213 | let mut sell_swap_ix_data = [0u8; 16];
214 | // Lay out the configs
215 | let swap_ix_hash = solana_program::hash::hash(b"global:swap");
216 | let buy_amount_as_bytes: [u8; 8] = buy_amount.to_le_bytes();
217 | let sell_amount_as_bytes: [u8; 8] = sell_amount.to_le_bytes();
218 | // Copy in the bytes
219 | buy_swap_ix_data[..8].copy_from_slice(&swap_ix_hash.to_bytes()[..8]);
220 | buy_swap_ix_data[8..].copy_from_slice(&buy_amount_as_bytes);
221 | sell_swap_ix_data[..8].copy_from_slice(&swap_ix_hash.to_bytes()[..8]);
222 | sell_swap_ix_data[8..].copy_from_slice(&sell_amount_as_bytes);
223 | (buy_swap_ix_data, sell_swap_ix_data)
224 | }
225 |
--------------------------------------------------------------------------------
/program/src/error.rs:
--------------------------------------------------------------------------------
1 | //! Arbitrage bot errors
2 | #[derive(Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)]
3 | pub enum ArbitrageProgramError {
4 | /// Invalid list of accounts: Each list of accounts should be the same
5 | /// length and passed in the following order: user token accounts, swap 1
6 | /// token accounts, swap 2 token accounts, mints
7 | #[error("Invalid list of accounts: Each list of accounts should be the same length and passed in the following order: user token accounts, swap 1 token accounts, swap 2 token accounts, mints")]
8 | InvalidAccountsList,
9 | /// A token account not belonging to the user, swap #1's Liquidity Pool, or
10 | /// swap #2's Liquidity Pool was passed into the program
11 | #[error("A token account not belonging to the user, swap #1's Liquidity Pool, or swap #2's Liquidity Pool was passed into the program")]
12 | TokenAccountOwnerNotFound,
13 | /// The user's proposed pay amount resolves to a value for `r` that exceeds
14 | /// the balance of the pool's token account for the receive asset
15 | #[error("The amount proposed to pay resolves to a receive amount that is greater than the current liquidity")]
16 | InvalidSwapNotEnoughLiquidity,
17 | /// No arbitrage opportunity was detected, so the program will return an
18 | /// error so that preflight fails
19 | #[error("No arbitrage opportunity detected")]
20 | NoArbitrage,
21 | }
22 |
23 | impl From for solana_program::program_error::ProgramError {
24 | fn from(e: ArbitrageProgramError) -> Self {
25 | solana_program::program_error::ProgramError::Custom(e as u32)
26 | }
27 | }
28 | impl solana_program::decode_error::DecodeError for ArbitrageProgramError {
29 | fn type_of() -> &'static str {
30 | "ArbitrageProgramError"
31 | }
32 | }
33 |
34 | impl solana_program::program_error::PrintProgramError for ArbitrageProgramError {
35 | fn print(&self)
36 | where
37 | E: 'static
38 | + std::error::Error
39 | + solana_program::decode_error::DecodeError
40 | + solana_program::program_error::PrintProgramError
41 | + num_traits::FromPrimitive,
42 | {
43 | match self {
44 | ArbitrageProgramError::InvalidAccountsList => {
45 | solana_program::msg!("Invalid list of accounts: Each list of accounts should be the same length and passed in the following order: user token accounts, swap 1 token accounts, swap 2 token accounts, mints")
46 | }
47 | ArbitrageProgramError::TokenAccountOwnerNotFound => {
48 | solana_program::msg!("A token account not belonging to the user, swap #1's Liquidity Pool, or swap #2's Liquidity Pool was passed into the program")
49 | }
50 | ArbitrageProgramError::InvalidSwapNotEnoughLiquidity => {
51 | solana_program::msg!("The amount proposed to pay resolves to a receive amount that is greater than the current liquidity")
52 | }
53 | ArbitrageProgramError::NoArbitrage => {
54 | solana_program::msg!("No arbitrage opportunity detected")
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/program/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Arbitrage bot between two swap programs!
2 | mod arb;
3 | mod error;
4 | mod partial_state;
5 | mod processor;
6 | mod swap;
7 | mod util;
8 |
9 | use borsh::{BorshDeserialize, BorshSerialize};
10 | use solana_program::{
11 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, program_error::ProgramError,
12 | pubkey::Pubkey,
13 | };
14 |
15 | /// The program's instructions
16 | /// This program only takes one instruction
17 | #[derive(BorshSerialize, BorshDeserialize, Debug)]
18 | pub enum ArbitrageProgramInstruction {
19 | TryArbitrage {
20 | /// The program ID of the first swap we want to inspect for arbitrage
21 | swap_1_program_id: Pubkey,
22 | /// The program ID of the second swap we want to inspect for arbitrage
23 | swap_2_program_id: Pubkey,
24 | /// How many assets we are going to evaluate combinations of at one time
25 | concurrency: u8,
26 | /// How aggressive the model will be when identifying arbitrage
27 | /// opportunities
28 | ///
29 | /// More specifically, a higher temperature will
30 | /// mean a smaller percent difference in swap values will trigger a
31 | /// trade
32 | temperature: u8,
33 | },
34 | }
35 |
36 | entrypoint!(process);
37 |
38 | /// Processor
39 | fn process(_program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
40 | match ArbitrageProgramInstruction::try_from_slice(data) {
41 | Ok(ix) => match ix {
42 | ArbitrageProgramInstruction::TryArbitrage {
43 | swap_1_program_id,
44 | swap_2_program_id,
45 | concurrency,
46 | temperature,
47 | } => processor::process_arbitrage(
48 | accounts,
49 | &swap_1_program_id,
50 | &swap_2_program_id,
51 | concurrency,
52 | temperature,
53 | ),
54 | },
55 | Err(_) => Err(ProgramError::InvalidInstructionData),
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/program/src/partial_state.rs:
--------------------------------------------------------------------------------
1 | //! Bytemuck-powered zero-copy partial deserialization
2 | use bytemuck::{Pod, Zeroable};
3 | use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey};
4 | use spl_token_2022::pod::OptionalNonZeroPubkey;
5 |
6 | use crate::error::ArbitrageProgramError;
7 |
8 | /// The first three fields of the `spl_token::state::Account`
9 | #[repr(C)]
10 | #[derive(Clone, Copy, Debug, Pod, Zeroable)]
11 | pub struct PartialTokenAccountState {
12 | pub mint: Pubkey,
13 | pub owner: Pubkey,
14 | pub amount: u64,
15 | }
16 |
17 | /// Custom return type for our arbitrage algorithm:
18 | /// (account, mint, owner, amount)
19 | pub type ArbitrageTokenAccountInfo<'a, 'b> = (&'a AccountInfo<'b>, Pubkey, Pubkey, u64);
20 |
21 | impl PartialTokenAccountState {
22 | /// Attempts to use zero-copy deserialization via Bytemuck to determine if
23 | /// this account is in fact an associated token account
24 | ///
25 | /// If it is, it will also validate the token account's owner against the
26 | /// provided address, and finally return only the vital information we need
27 | /// for the rest of the arbitrage program
28 | pub fn try_deserialize<'a, 'b>(
29 | account_info: &'a AccountInfo<'b>,
30 | owner: &'a Pubkey,
31 | ) -> Result, ProgramError> {
32 | // Check that the account has enough data to try to deserialize
33 | if account_info.data_len() < 72 {
34 | msg!(
35 | "Data too small. Should be 72. Found len: {}",
36 | account_info.data_len()
37 | );
38 | msg!("Token Account: {}", account_info.key);
39 | return Err(ArbitrageProgramError::InvalidAccountsList.into());
40 | }
41 | // Try to partially deserialize the account data
42 | match bytemuck::try_from_bytes::(&account_info.data.borrow()[..72]) {
43 | // Validate the owner
44 | Ok(partial_token) => {
45 | if !partial_token.owner.eq(owner) {
46 | msg!("Owner mismatch");
47 | msg!("Expected: {}", owner);
48 | msg!("Got: {}", partial_token.owner);
49 | msg!("Token Account: {}", account_info.key);
50 | return Err(ArbitrageProgramError::InvalidAccountsList.into());
51 | }
52 | // Return the vital information
53 | Ok((
54 | account_info,
55 | partial_token.mint,
56 | partial_token.owner,
57 | partial_token.amount,
58 | ))
59 | }
60 | Err(_) => {
61 | msg!("Failed to deserialize associated token account");
62 | msg!("Token Account: {}", account_info.key);
63 | Err(ArbitrageProgramError::InvalidAccountsList.into())
64 | }
65 | }
66 | }
67 | }
68 |
69 | /// The first two fields of the `spl_token::state::Mint`
70 | ///
71 | /// The third field `decimals` - which is the one we are interested in - cannot
72 | /// be included in this struct since Bytemuck will not allow two integer types
73 | /// of varying size - such as `u64` and `u8`
74 | ///
75 | /// However, since `decimals` is a single byte (`u8`), we can simply take the
76 | /// next byte if the data deserializes into this struct properly
77 | #[repr(C)]
78 | #[derive(Clone, Copy, Debug, Pod, Zeroable)]
79 | pub struct PartialMintState {
80 | pub mint_authority: OptionalNonZeroPubkey,
81 | pub supply: u64,
82 | }
83 |
84 | /// Custom return type for our arbitrage algorithm:
85 | /// (account, decimals)
86 | pub type ArbitrageMintInfo<'a, 'b> = (&'a AccountInfo<'b>, u8);
87 |
88 | impl PartialMintState {
89 | /// Attempts to use zero-copy deserialization via Bytemuck to determine if
90 | /// this account is in fact a mint account
91 | ///
92 | /// If it is, it will return only the vital information we need
93 | /// for the rest of the arbitrage program
94 | pub fn try_deserialize<'a, 'b>(
95 | account_info: &'a AccountInfo<'b>,
96 | ) -> Result, ProgramError> {
97 | // Check that the account has enough data to try to deserialize
98 | if account_info.data_len() < 41 {
99 | msg!(
100 | "Data too small. Should be 41. Found len: {}",
101 | account_info.data_len()
102 | );
103 | msg!("Mint: {}", account_info.key);
104 | return Err(ArbitrageProgramError::InvalidAccountsList.into());
105 | }
106 | let data = &account_info.data.borrow()[..41];
107 | // Try to partially deserialize the account data
108 | match bytemuck::try_from_bytes::(&data[..40]) {
109 | Ok(_) => {
110 | let decimals = match data.get(40) {
111 | Some(d) => d,
112 | None => {
113 | msg!("Could not get decimals");
114 | msg!("Mint: {}", account_info.key);
115 | return Err(ArbitrageProgramError::InvalidAccountsList.into());
116 | }
117 | };
118 | // Return the vital information
119 | Ok((account_info, *decimals))
120 | }
121 | Err(_) => {
122 | msg!("Failed to deserialize mint account");
123 | msg!("Mint: {}", account_info.key);
124 | Err(ArbitrageProgramError::InvalidAccountsList.into())
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/program/src/processor.rs:
--------------------------------------------------------------------------------
1 | //! Processes an attempt to arbitrage trade
2 | use solana_program::{
3 | account_info::{next_account_info, AccountInfo},
4 | entrypoint::ProgramResult,
5 | pubkey::Pubkey,
6 | };
7 |
8 | use crate::arb::{try_arbitrage, TryArbitrageArgs};
9 | use crate::partial_state::{PartialMintState, PartialTokenAccountState};
10 | use crate::util::check_pool_address;
11 |
12 | /// Processes program inputs to search for an arbitrage opportunity between two
13 | /// swap programs
14 | ///
15 | /// Note: accounts must be provided in a very specific order:
16 | /// * Payer
17 | /// * Token Program
18 | /// * Swap #1 Liquidity Pool
19 | /// * Swap #2 Liquidity Pool
20 | /// * [Token Accounts for User]
21 | /// * [Token Accounts for Swap #1]
22 | /// * [Token Accounts for Swap #2]
23 | /// * [Mint Accounts]
24 | pub fn process_arbitrage(
25 | accounts: &[AccountInfo],
26 | swap_1_program_id: &Pubkey,
27 | swap_2_program_id: &Pubkey,
28 | concurrency: u8,
29 | temperature: u8,
30 | ) -> ProgramResult {
31 | // Load the first few "fixed" accounts provided
32 | let accounts_iter = &mut accounts.iter();
33 | let payer = next_account_info(accounts_iter)?;
34 | let token_program = next_account_info(accounts_iter)?;
35 | let system_program = next_account_info(accounts_iter)?;
36 | let associated_token_program = next_account_info(accounts_iter)?;
37 | let swap_1_program = next_account_info(accounts_iter)?;
38 | let swap_2_program = next_account_info(accounts_iter)?;
39 | let swap_1_pool = next_account_info(accounts_iter)?;
40 | let swap_2_pool = next_account_info(accounts_iter)?;
41 |
42 | // Ensure each pool address follows the correct derivation from its
43 | // corresponding program ID
44 | check_pool_address(swap_1_program_id, swap_1_pool.key)?;
45 | check_pool_address(swap_2_program_id, swap_2_pool.key)?;
46 |
47 | // Read the provided user's token accounts
48 | let token_accounts_user = {
49 | let mut accts = vec![];
50 | for _x in 0..concurrency {
51 | accts.push(PartialTokenAccountState::try_deserialize(
52 | next_account_info(accounts_iter)?,
53 | payer.key,
54 | )?);
55 | }
56 | accts
57 | };
58 |
59 | // Read the provided token accounts for Swap Program #1
60 | let token_accounts_swap_1 = {
61 | let mut accts = vec![];
62 | for _x in 0..concurrency {
63 | accts.push(PartialTokenAccountState::try_deserialize(
64 | next_account_info(accounts_iter)?,
65 | swap_1_pool.key,
66 | )?);
67 | }
68 | accts
69 | };
70 |
71 | // Read the provided token accounts for Swap Program #2
72 | let token_accounts_swap_2 = {
73 | let mut accts = vec![];
74 | for _x in 0..concurrency {
75 | accts.push(PartialTokenAccountState::try_deserialize(
76 | next_account_info(accounts_iter)?,
77 | swap_2_pool.key,
78 | )?);
79 | }
80 | accts
81 | };
82 |
83 | // Read the provided mint accounts for the assets to evaluate all combinations
84 | let mints = {
85 | let mut accts = vec![];
86 | for _x in 0..concurrency {
87 | accts.push(PartialMintState::try_deserialize(next_account_info(
88 | accounts_iter,
89 | )?)?);
90 | }
91 | accts
92 | };
93 |
94 | // Check if there is an arbitrage opportunity between the two pools, and
95 | // execute the trade if there is one
96 | try_arbitrage(TryArbitrageArgs {
97 | token_accounts_user,
98 | token_accounts_swap_1,
99 | token_accounts_swap_2,
100 | mints,
101 | payer,
102 | token_program,
103 | system_program,
104 | associated_token_program,
105 | swap_1_program,
106 | swap_2_program,
107 | swap_1_pool,
108 | swap_2_pool,
109 | temperature,
110 | })
111 | }
112 |
--------------------------------------------------------------------------------
/program/src/swap.rs:
--------------------------------------------------------------------------------
1 | //! Swap functions copied from the Swap program
2 | use solana_program::program_error::ProgramError;
3 | use std::ops::{Add, Div, Mul};
4 |
5 | use crate::error::ArbitrageProgramError;
6 |
7 | /// The constant-product algorithm `f(p)` to determine the allowed amount of the
8 | /// receiving asset that can be returned in exchange for the amount of the paid
9 | /// asset offered
10 | ///
11 | /// ```
12 | /// K = a * b * c * d * P * R
13 | /// K = a * b * c * d * (P + p) * (R - r)
14 | ///
15 | /// a * b * c * d * P * R = a * b * c * d * (P + p) * (R - r)
16 | /// PR = (P + p) * (R - r)
17 | /// PR = PR - Pr + Rp - pr
18 | /// 0 = 0 - Pr + Rp - pr
19 | /// -Rp = -Pr - pr
20 | /// -Rp = r(-P - p)
21 | /// r = (-Rp) / (-P - p)
22 | /// r = [-1 * Rp] / [-1 * (P + p)]
23 | /// r = Rp / (P + p)
24 | ///
25 | /// r = f(p) = (R * p) / (P + p)
26 | /// ```
27 | pub fn determine_swap_receive(
28 | pool_recieve_balance: u64,
29 | receive_decimals: u8,
30 | pool_pay_balance: u64,
31 | pay_decimals: u8,
32 | pay_amount: u64,
33 | ) -> Result {
34 | // Convert all values to nominal floats using their respective mint decimal
35 | // places
36 | let big_r = convert_to_float(pool_recieve_balance, receive_decimals);
37 | let big_p = convert_to_float(pool_pay_balance, pay_decimals);
38 | let p = convert_to_float(pay_amount, pay_decimals);
39 | // Calculate `f(p)` to get `r`
40 | let bigr_times_p = big_r.mul(p);
41 | let bigp_plus_p = big_p.add(p);
42 | let r = bigr_times_p.div(bigp_plus_p);
43 | // Make sure `r` does not exceed liquidity
44 | if r > big_r {
45 | return Err(ArbitrageProgramError::InvalidSwapNotEnoughLiquidity.into());
46 | }
47 | // Return the real value of `r`
48 | Ok(convert_from_float(r, receive_decimals))
49 | }
50 |
51 | /// Converts a `u64` value - in this case the balance of a token account - into
52 | /// an `f32` by using the `decimals` value of its associated mint to get the
53 | /// nominal quantity of a mint stored in that token account
54 | ///
55 | /// For example, a token account with a balance of 10,500 for a mint with 3
56 | /// decimals would have a nominal balance of 10.5
57 | fn convert_to_float(value: u64, decimals: u8) -> f32 {
58 | (value as f32).div(f32::powf(10.0, decimals as f32))
59 | }
60 |
61 | /// Converts a nominal value - in this case the calculated value `r` - into a
62 | /// `u64` by using the `decimals` value of its associated mint to get the real
63 | /// quantity of the mint that the user will receive
64 | ///
65 | /// For example, if `r` is calculated to be 10.5, the real amount of the asset
66 | /// to be received by the user is 10,500
67 | fn convert_from_float(value: f32, decimals: u8) -> u64 {
68 | value.mul(f32::powf(10.0, decimals as f32)) as u64
69 | }
70 |
--------------------------------------------------------------------------------
/program/src/util.rs:
--------------------------------------------------------------------------------
1 | //! Util functions for arbitrage bot
2 | use solana_program::{
3 | account_info::AccountInfo, entrypoint::ProgramResult, instruction::AccountMeta,
4 | program_error::ProgramError, pubkey::Pubkey,
5 | };
6 |
7 | use crate::error::ArbitrageProgramError;
8 |
9 | // Asserts the pool address provided is in fact derived from the program ID
10 | // provided
11 | pub fn check_pool_address(program_id: &Pubkey, pool: &Pubkey) -> ProgramResult {
12 | if !Pubkey::find_program_address(&[b"liquidity_pool"], program_id)
13 | .0
14 | .eq(pool)
15 | {
16 | return Err(solana_program::program_error::ProgramError::InvalidInstructionData);
17 | }
18 | Ok(())
19 | }
20 |
21 | /// Trait used to unpack `Option` values for smoother algorithm code
22 | pub trait ArbitrageEvaluateOption {
23 | fn ok_or_arb_err(self) -> Result;
24 | }
25 |
26 | impl ArbitrageEvaluateOption for Option {
27 | fn ok_or_arb_err(self) -> Result {
28 | self.ok_or(ArbitrageProgramError::InvalidAccountsList.into())
29 | }
30 | }
31 |
32 | /// Trait used to convert from an `AccountInfo` to an `AccountMeta`
33 | pub trait ToAccountMeta {
34 | fn to_account_meta(&self) -> AccountMeta;
35 | }
36 |
37 | impl ToAccountMeta for AccountInfo<'_> {
38 | fn to_account_meta(&self) -> AccountMeta {
39 | AccountMeta {
40 | pubkey: *self.key,
41 | is_signer: self.is_signer,
42 | is_writable: self.is_writable,
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | comment_width = 80
2 | wrap_comments = true
--------------------------------------------------------------------------------
/tests/main.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getAccount as getTokenAccount,
3 | getAssociatedTokenAddressSync,
4 | TokenAccountNotFoundError,
5 | createAccount as createTokenAccount,
6 | } from '@solana/spl-token'
7 | import { PublicKey, SendTransactionError } from '@solana/web3.js'
8 | import { sleepSeconds } from './util'
9 | import assetsConfig from './util/assets.json'
10 | import { ARBITRAGE_PROGRAM, CONNECTION, PAYER } from './util/const'
11 | import { createArbitrageInstruction, getPoolAddress } from './util/instruction'
12 | import { mintExistingTokens } from './util/token'
13 | import {
14 | buildTransactionV0,
15 | buildTransactionV0WithLookupTable,
16 | createAddressLookupTable,
17 | extendAddressLookupTable,
18 | printAddressLookupTable,
19 | } from './util/transaction'
20 | import { before, describe, it } from 'mocha'
21 |
22 | // Swap programs to arbitrage trade
23 | const SWAP_PROGRAM_1 = new PublicKey(
24 | '5koF84vG5xwah17PNRyge3HmqdJZ4rqdqPvZnMKqi8Bq'
25 | )
26 | const SWAP_PROGRAM_2 = new PublicKey(
27 | 'DRP4K7yv8EBftb3roP81idoPtRDJwpak1Apw8d4Df14T'
28 | )
29 |
30 | // Temperature `t`: How aggressive should the model be? 0..99
31 | const temperature = 60
32 | // Concurrency `n`: Try `n` assets at a time
33 | const concurrency = 8
34 | // Iterations `i`: Check all asset pairings `i` times
35 | const iterations = 2
36 |
37 | /**
38 | * Test the Arbitrage Program
39 | */
40 | describe('Arbitrage Bot', async () => {
41 | const connection = CONNECTION
42 | const payer = PAYER
43 | const arbProgram = ARBITRAGE_PROGRAM
44 | const assets = assetsConfig.assets.map((o) => {
45 | return {
46 | name: o.name,
47 | quantity: o.quantity,
48 | decimals: o.decimals,
49 | address: new PublicKey(o.address),
50 | }
51 | })
52 |
53 | // The address of our Address Lookup Table, which we'll set when we create the table
54 | let lookupTable: PublicKey
55 |
56 | // The lists of accounts required for the arbitrage program, which we'll build
57 | // as we iterate through the list of assets in `util/assets.json` and derive the
58 | // associated token accounts in the `before` step
59 | let tokenAccountsUser: PublicKey[] = []
60 | let tokenAccountsSwap1: PublicKey[] = []
61 | let tokenAccountsSwap2: PublicKey[] = []
62 | let mints: PublicKey[] = []
63 |
64 | /**
65 | * Collects all necessary accounts and mints tokens to the payer if necessary
66 | */
67 | before(
68 | 'Collect all token accounts & mints and mint some assets to the payer if necessary',
69 | async () => {
70 | for (const a of assets) {
71 | const tokenAddressUser = getAssociatedTokenAddressSync(
72 | a.address,
73 | payer.publicKey
74 | )
75 | try {
76 | // Check if the token account holds any tokens currently
77 | const tokenAccount = await getTokenAccount(
78 | connection,
79 | tokenAddressUser
80 | )
81 | if (tokenAccount.amount === BigInt(0)) {
82 | // If not, mint some tokens to it
83 | await mintExistingTokens(
84 | connection,
85 | payer,
86 | a.address,
87 | 10,
88 | a.decimals
89 | )
90 | }
91 | } catch (e) {
92 | // Catch the error if the token account doesn't exist
93 | if (e === TokenAccountNotFoundError) {
94 | // Create the token account
95 | await createTokenAccount(
96 | connection,
97 | payer,
98 | a.address,
99 | payer.publicKey
100 | )
101 | // Mint some tokens to it
102 | await mintExistingTokens(
103 | connection,
104 | payer,
105 | a.address,
106 | 10,
107 | a.decimals
108 | )
109 | }
110 | }
111 | // Add each account to its respective list
112 | tokenAccountsUser.push(tokenAddressUser)
113 | tokenAccountsSwap1.push(
114 | getAssociatedTokenAddressSync(
115 | a.address,
116 | getPoolAddress(SWAP_PROGRAM_1),
117 | true
118 | )
119 | )
120 | tokenAccountsSwap2.push(
121 | getAssociatedTokenAddressSync(
122 | a.address,
123 | getPoolAddress(SWAP_PROGRAM_2),
124 | true
125 | )
126 | )
127 | mints.push(a.address)
128 | }
129 | }
130 | )
131 |
132 | /**
133 | * Creates the Address Lookup Table for our arbitrage instruction
134 | */
135 | it('Create a Lookup Table', async () => {
136 | lookupTable = await createAddressLookupTable(connection, payer)
137 | await sleepSeconds(2)
138 | // Helper function to avoid code repetition
139 | //
140 | // We have to send each list one at a time, since sending all addresses
141 | // would max out the transaction size limit
142 | async function inlineExtend(addresses: PublicKey[]) {
143 | await extendAddressLookupTable(
144 | connection,
145 | payer,
146 | lookupTable,
147 | addresses
148 | )
149 | await sleepSeconds(2)
150 | }
151 | inlineExtend(tokenAccountsUser)
152 | inlineExtend(tokenAccountsSwap1)
153 | inlineExtend(tokenAccountsSwap2)
154 | inlineExtend(mints)
155 | printAddressLookupTable(connection, lookupTable)
156 | })
157 |
158 | /**
159 | * Function to send the arbitrage instruction to the program
160 | * To be used in the loop below
161 | */
162 | async function sendArbitrageInstruction(
163 | tokenAccountsUserSubList: PublicKey[],
164 | tokenAccountsSwap1SubList: PublicKey[],
165 | tokenAccountsSwap2SubList: PublicKey[],
166 | mintsSubList: PublicKey[],
167 | concurrencyVal: number
168 | ) {
169 | const ix = createArbitrageInstruction(
170 | arbProgram.publicKey,
171 | payer.publicKey,
172 | tokenAccountsUserSubList,
173 | tokenAccountsSwap1SubList,
174 | tokenAccountsSwap2SubList,
175 | mintsSubList,
176 | concurrencyVal,
177 | temperature,
178 | SWAP_PROGRAM_1,
179 | SWAP_PROGRAM_2
180 | )
181 | const tx = await buildTransactionV0WithLookupTable(
182 | connection,
183 | [ix],
184 | payer.publicKey,
185 | [payer],
186 | lookupTable
187 | )
188 | // const txNoLT = await buildTransactionV0(
189 | // connection,
190 | // [ix],
191 | // payer.publicKey,
192 | // [payer]
193 | // )
194 | console.log(`Sending transaction with ${concurrencyVal} accounts...`)
195 | console.log(`Tx size with Lookup Table : ${tx.serialize().length}`)
196 | // console.log(
197 | // `Tx size WITHOUT Lookup Table : ${txNoLT.serialize().length}`
198 | // )
199 | try {
200 | await connection.sendTransaction(tx, { skipPreflight: true })
201 | console.log('====================================')
202 | console.log(' Arbitrage trade placed!')
203 | console.log('====================================')
204 | } catch (error) {
205 | if (error instanceof SendTransactionError) {
206 | if (error.message.includes('custom program error: 0x3')) {
207 | console.log('====================================')
208 | console.log(' No arbitrage opportunity found')
209 | console.log('====================================')
210 | } else {
211 | throw error
212 | }
213 | } else {
214 | throw error
215 | }
216 | }
217 | await sleepSeconds(2)
218 | }
219 |
220 | /**
221 | * Hit our arbitrage program with some of the total list of accounts,
222 | * based on the `concurrency` config, and see if we can obtain arbitrage
223 | * opportunities
224 | */
225 | it('Try Arbitrage', async () => {
226 | await sleepSeconds(4)
227 | // Loop through number of `iterations`
228 | for (let x = 0; x < iterations; x++) {
229 | console.log(`Iteration: ${x + 1}`)
230 | let len = mints.length
231 | // Loop through all combinations of assets based on `concurrency`
232 | let step = 0
233 | let brake = concurrency
234 | let tokenAccountsUserSubList = []
235 | let tokenAccountsSwap1SubList = []
236 | let tokenAccountsSwap2SubList = []
237 | let mintsSubList = []
238 | for (let i = 0; i < len; i++) {
239 | for (let j = i; j < len; j++) {
240 | if (step == brake) {
241 | // Send an arbitrage instruction to the program
242 | const end = brake + concurrency
243 | await sendArbitrageInstruction(
244 | tokenAccountsUserSubList,
245 | tokenAccountsSwap1SubList,
246 | tokenAccountsSwap2SubList,
247 | mintsSubList,
248 | end - brake
249 | )
250 | await sleepSeconds(2)
251 | brake = end
252 | tokenAccountsUserSubList = []
253 | tokenAccountsSwap1SubList = []
254 | tokenAccountsSwap2SubList = []
255 | mintsSubList = []
256 | }
257 | // Build sub-lists of accounts to send to the arb program
258 | tokenAccountsUserSubList.push(tokenAccountsUser[j])
259 | tokenAccountsSwap1SubList.push(tokenAccountsSwap1[j])
260 | tokenAccountsSwap2SubList.push(tokenAccountsSwap2[j])
261 | mintsSubList.push(mints[j])
262 | step++
263 | }
264 | }
265 | // Send the instruction with any remaining accounts for this iteration
266 | if (mintsSubList.length! - 0) {
267 | await sendArbitrageInstruction(
268 | tokenAccountsUserSubList,
269 | tokenAccountsSwap1SubList,
270 | tokenAccountsSwap2SubList,
271 | mintsSubList,
272 | mintsSubList.length
273 | )
274 | }
275 | }
276 | })
277 | })
278 |
--------------------------------------------------------------------------------
/tests/util/assets.json:
--------------------------------------------------------------------------------
1 | {"assets":[{"name":"Cannon","symbol":"CAN","description":"A cannon for defending yer ship!","uri":"https://arweave.net/ArreGQ6DYdIPGs_KiwVXE7AMVclLliICTlls6Ff8DbY","decimals":3,"quantity":80,"address":"33YzMAsC6wwvFkBtDqsMXrRUjMofPLdrg5hj6NLNkHGz"},{"name":"Cannon Ball","symbol":"CANB","description":"Cannon balls for yer cannons!","uri":"https://arweave.net/ZlxqEw-BVEO7LKLWE7uKJoCxC67w-0zaO9ydX-m6y7A","decimals":9,"quantity":60,"address":"ARSNZPotMEht3FTaJe53sE1s9T1MEsRAUGzinn8YsQty"},{"name":"Compass","symbol":"COMP","description":"A compass to navigate the seven seas!","uri":"https://arweave.net/MvWxgCu231-0IrE3FWoaAYoFaD1MgWKOqDjCv5v6LEA","decimals":3,"quantity":20,"address":"2nKLMw3VLow5oUfUm4Q5xTRTxt468fwpDzBupcHGdB8n"},{"name":"Fishing Net","symbol":"FISH","description":"A fishing net for catching meals for the crew!","uri":"https://arweave.net/2wCk8mVH8KyqwbYLlxae9kBBKb4jTRPeiXVrqUTvjUE","decimals":3,"quantity":50,"address":"9p7YuX5QtovxWRRSFp98GvRj9xZzbrUng4MPoDUKHAPX"},{"name":"Gold","symbol":"GOLD","description":"Ahh the finest gold in all of these waters!","uri":"https://arweave.net/46B9y63MXlLnprZLvrRjS_50yecOydiLbxTbieAREBk","decimals":6,"quantity":500,"address":"4q2GV6wScZsf84rRX5gcbH4E8dTuAq8ZvRyALphgSevP"},{"name":"Grappling Hook","symbol":"GRAP","description":"A grappling hook for boarding other ships!","uri":"https://arweave.net/RT9sF6ENI__DQGYD74H4SdncsjLyeO1Rm3dRQ1T1Tz0","decimals":3,"quantity":50,"address":"5v14TdzH3T1rkh53pYtdQTNUj3WHf1Kv6NJ3pnfKbnH5"},{"name":"Gunpowder","symbol":"GUNP","description":"Gunpowder for ye muskets!","uri":"https://arweave.net/eLqSxbbW7ATsQioluBZt9KG0YnU0jRoR_46g0EJU75Q","decimals":9,"quantity":60,"address":"FEmJ5XfxWh8g3DfdSDL4deLSkKyB7zNTw36DxzDf5jEW"},{"name":"Musket","symbol":"MUSK","description":"A musket for firing on enemies!","uri":"https://arweave.net/tKdMfImOkHEqo8xWulIv92MybdpkbnWjDJmazj3lTpU","decimals":3,"quantity":60,"address":"Eysuh7HP8NgzzSe5eVJZ6bei8DPtVJ2jQrjk9vqmeTb4"},{"name":"Rum","symbol":"RUM","description":"Rum, more rum!","uri":"https://arweave.net/NJGvUP2EuK-LJ7QV6qRifcGC8ao2Vd9WSW2GjiGSpVw","decimals":6,"quantity":100,"address":"6KVLM9utNr5WKsoAtS8u1e5ZaJu7u54pTKZkb2UT8avU"},{"name":"Telescope","symbol":"TELE","description":"A telescope for spotting booty amongst the seas!","uri":"https://arweave.net/NmBFKuCWk4yfAB4a6EGwXlWM92q5B9nBZMlRhpVA4y4","decimals":3,"quantity":20,"address":"GAdBdhPc73ho3qeEAeJg3ML6monHByUkUuBaFNXJnqQY"},{"name":"Treasure Map","symbol":"TMAP","description":"A map to help ye find long lost treasures!","uri":"https://arweave.net/_cM90jcVdRRYt-OA8xIV--gZCAOpe18BCLvV2TVW7Fc","decimals":3,"quantity":10,"address":"9cu1QkUhtDNQa59j2oEsXpkFdUX8oGiY5aZknKYa6mCE"}]}
--------------------------------------------------------------------------------
/tests/util/const.ts:
--------------------------------------------------------------------------------
1 | import { Connection } from '@solana/web3.js'
2 | import { loadKeypairFromFile } from '.'
3 | import os from 'os'
4 |
5 | // RPC connection
6 | export const CONNECTION = new Connection('http://localhost:8899', 'confirmed')
7 | // export const CONNECTION = new Connection(
8 | // 'https://api.devnet.solana.com',
9 | // 'confirmed'
10 | // )
11 |
12 | // Local keypair
13 | export const PAYER = loadKeypairFromFile(os.homedir + '/.config/solana/id.json')
14 |
15 | // Arbitrage program
16 | export const ARBITRAGE_PROGRAM = loadKeypairFromFile(
17 | './target/deploy/arb_program-keypair.json'
18 | )
19 |
--------------------------------------------------------------------------------
/tests/util/index.ts:
--------------------------------------------------------------------------------
1 | import { Keypair } from '@solana/web3.js'
2 | import fs from 'fs'
3 |
4 | /**
5 | *
6 | * Reads a Keypair JSON file from the local machine
7 | *
8 | * @param path Path to keypair JSON file
9 | * @returns The Solana keypair as a `Keypair` object
10 | */
11 | export function loadKeypairFromFile(path: string): Keypair {
12 | return Keypair.fromSecretKey(
13 | Buffer.from(JSON.parse(fs.readFileSync(path, 'utf-8')))
14 | )
15 | }
16 |
17 | /**
18 | *
19 | * Sleeps the process `s` seconds
20 | *
21 | * @param s Seconds to sleep
22 | * @returns Promise - causes the script to sleep
23 | */
24 | export function sleepSeconds(s: number) {
25 | return new Promise((resolve) => setTimeout(resolve, s * 1000))
26 | }
27 |
--------------------------------------------------------------------------------
/tests/util/instruction.ts:
--------------------------------------------------------------------------------
1 | import * as borsh from 'borsh'
2 | import { Buffer } from 'buffer'
3 | import {
4 | AccountMeta,
5 | PublicKey,
6 | SystemProgram,
7 | SYSVAR_RENT_PUBKEY,
8 | TransactionInstruction,
9 | } from '@solana/web3.js'
10 | import {
11 | TOKEN_PROGRAM_ID,
12 | ASSOCIATED_TOKEN_PROGRAM_ID,
13 | } from '@solana/spl-token'
14 |
15 | /**
16 | * Get the PDA of the Liquidity Pool for a program
17 | */
18 | export function getPoolAddress(programId: PublicKey): PublicKey {
19 | return PublicKey.findProgramAddressSync(
20 | [Buffer.from('liquidity_pool')],
21 | programId
22 | )[0]
23 | }
24 |
25 | /**
26 | * Arbitrage program instructions
27 | */
28 | class ArbitrageProgramInstruction {
29 | instruction: number
30 | swap_1_program_id: Uint8Array
31 | swap_2_program_id: Uint8Array
32 | concurrency: number
33 | temperature: number
34 | constructor(props: {
35 | swapProgram1: PublicKey
36 | swapProgram2: PublicKey
37 | concurrency: number
38 | temperature: number
39 | }) {
40 | this.instruction = 0
41 | this.swap_1_program_id = props.swapProgram1.toBuffer()
42 | this.swap_2_program_id = props.swapProgram2.toBuffer()
43 | this.concurrency = props.concurrency
44 | this.temperature = props.temperature
45 | }
46 | toBuffer() {
47 | return Buffer.from(
48 | borsh.serialize(ArbitrageProgramInstructionSchema, this)
49 | )
50 | }
51 | }
52 |
53 | const ArbitrageProgramInstructionSchema = new Map([
54 | [
55 | ArbitrageProgramInstruction,
56 | {
57 | kind: 'struct',
58 | fields: [
59 | ['instruction', 'u8'],
60 | ['swap_1_program_id', [32]],
61 | ['swap_2_program_id', [32]],
62 | ['concurrency', 'u8'],
63 | ['temperature', 'u8'],
64 | ],
65 | },
66 | ],
67 | ])
68 |
69 | /**
70 | *
71 | * "Default" `AccountMeta` (marks as mutable non-signer)
72 | * Used for Token Accounts to "lock" them on the Sealevel runtime
73 | *
74 | * @param pubkey Address of the account
75 | * @returns `KeyArg`
76 | */
77 | export function defaultAccountMeta(pubkey: PublicKey): AccountMeta {
78 | return { pubkey, isSigner: false, isWritable: true }
79 | }
80 |
81 | /**
82 | *
83 | * Creates the instruction for our Arbitrage Program
84 | *
85 | * @param programId Arbitrage program ID
86 | * @param payer Token payer (the one funding the arb)
87 | * @param tokenAccountsUser The payer's token accounts
88 | * @param tokenAccountsSwap1 Swap #1's token accounts
89 | * @param tokenAccountsSwap2 Swap #2's token accounts
90 | * @param mints The asset mints
91 | * @param concurrency How many accounts we're evaluating at once
92 | * @param swapProgram1 Swap #1 program ID
93 | * @param swapProgram2 Swap #2 program ID
94 | * @returns `TransactionInstruction`
95 | */
96 | export function createArbitrageInstruction(
97 | programId: PublicKey,
98 | payer: PublicKey,
99 | tokenAccountsUser: PublicKey[],
100 | tokenAccountsSwap1: PublicKey[],
101 | tokenAccountsSwap2: PublicKey[],
102 | mints: PublicKey[],
103 | concurrency: number,
104 | temperature: number,
105 | swapProgram1: PublicKey,
106 | swapProgram2: PublicKey
107 | ): TransactionInstruction {
108 | let swapPool1 = getPoolAddress(swapProgram1)
109 | let swapPool2 = getPoolAddress(swapProgram2)
110 | const data = new ArbitrageProgramInstruction({
111 | swapProgram1,
112 | swapProgram2,
113 | concurrency,
114 | temperature,
115 | }).toBuffer()
116 | let keys: AccountMeta[] = [
117 | // Payer
118 | { pubkey: payer, isSigner: true, isWritable: true },
119 | // Token Program
120 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
121 | // System Program
122 | { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
123 | // Associated Token Program
124 | {
125 | pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
126 | isSigner: false,
127 | isWritable: false,
128 | },
129 | // Swap #1 Program
130 | { pubkey: swapProgram1, isSigner: false, isWritable: false },
131 | // Swap #2 Program
132 | { pubkey: swapProgram2, isSigner: false, isWritable: false },
133 | // Liquidity Pool for Swap #1
134 | defaultAccountMeta(swapPool1),
135 | // Liquidity Pool for Swap #2
136 | defaultAccountMeta(swapPool2),
137 | ]
138 | // [Token Accounts for User]
139 | tokenAccountsUser.forEach((a) => keys.push(defaultAccountMeta(a)))
140 | // [Token Accounts for Swap #1]
141 | tokenAccountsSwap1.forEach((a) => keys.push(defaultAccountMeta(a)))
142 | // [Token Accounts for Swap #2]
143 | tokenAccountsSwap2.forEach((a) => keys.push(defaultAccountMeta(a)))
144 | // [Mint Accounts]
145 | mints.forEach((a) => keys.push(defaultAccountMeta(a)))
146 |
147 | return new TransactionInstruction({
148 | keys,
149 | programId,
150 | data,
151 | })
152 | }
153 |
--------------------------------------------------------------------------------
/tests/util/token.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createAssociatedTokenAccountInstruction,
3 | createInitializeMintInstruction,
4 | createMintToInstruction,
5 | getAssociatedTokenAddressSync,
6 | MINT_SIZE,
7 | TOKEN_PROGRAM_ID,
8 | } from '@solana/spl-token'
9 | import { Connection, Keypair, PublicKey, SystemProgram } from '@solana/web3.js'
10 | import { buildTransactionV0 } from './transaction'
11 |
12 | /**
13 | *
14 | * Returns the real quantity of a `quantity` parameter by
15 | * increasing the number using the mint's decimal places
16 | *
17 | * @param quantity The provided quantity argument
18 | * @param decimals The decimals of the associated mint
19 | * @returns The real quantity of a `quantity` parameter
20 | */
21 | export function toBigIntQuantity(quantity: number, decimals: number): bigint {
22 | return BigInt(quantity) * BigInt(10) ** BigInt(decimals)
23 | }
24 |
25 | /**
26 | *
27 | * Returns the nominal quantity of a `quantity` parameter by
28 | * decreasing the number using the mint's decimal places
29 | *
30 | * @param quantity The real quantity of a `quantity` parameter
31 | * @param decimals The decimals of the associated mint
32 | * @returns The nominal quantity of a `quantity` parameter
33 | */
34 | export function fromBigIntQuantity(quantity: bigint, decimals: number): string {
35 | return (Number(quantity) / 10 ** decimals).toFixed(6)
36 | }
37 |
38 | /**
39 | *
40 | * Mints an existing SPL token to the local keypair
41 | *
42 | * @param connection
43 | * @param payer The Liquidity Provider (local wallet in `Anchor.toml`)
44 | * @param mint The asset's mint address
45 | * @param quantity The quantity to fund of the provided mint
46 | * @param decimals the decimals of this mint (used to calculate real quantity)
47 | */
48 | export async function mintExistingTokens(
49 | connection: Connection,
50 | payer: Keypair,
51 | mint: PublicKey,
52 | quantity: number,
53 | decimals: number
54 | ) {
55 | const tokenAccount = getAssociatedTokenAddressSync(mint, payer.publicKey)
56 | const mintToWalletIx = createMintToInstruction(
57 | mint,
58 | tokenAccount,
59 | payer.publicKey,
60 | toBigIntQuantity(quantity, decimals)
61 | )
62 | const tx = await buildTransactionV0(
63 | connection,
64 | [mintToWalletIx],
65 | payer.publicKey,
66 | [payer]
67 | )
68 | await connection.sendTransaction(tx)
69 | }
70 |
--------------------------------------------------------------------------------
/tests/util/transaction.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Connection,
3 | Keypair,
4 | PublicKey,
5 | TransactionInstruction,
6 | VersionedTransaction,
7 | TransactionMessage,
8 | AddressLookupTableProgram,
9 | AddressLookupTableAccount,
10 | } from '@solana/web3.js'
11 | import { sleepSeconds } from '.'
12 |
13 | /**
14 | *
15 | * Creates an Address Lookup Table
16 | *
17 | * @param connection Connection to Solana RPC
18 | * @param payer Transaction Fee Payer and Lookup Table Authority
19 | * @returns Address of the Lookup Table
20 | */
21 | export async function createAddressLookupTable(
22 | connection: Connection,
23 | payer: Keypair
24 | ): Promise {
25 | // You must use `max` for the derivation of the Lookup Table address to work consistently
26 | let recentSlot = await connection.getSlot('max')
27 | let [createLookupTableIx, lookupTable] =
28 | AddressLookupTableProgram.createLookupTable({
29 | authority: payer.publicKey,
30 | payer: payer.publicKey,
31 | recentSlot,
32 | })
33 | const tx = await buildTransactionV0(
34 | connection,
35 | [createLookupTableIx],
36 | payer.publicKey,
37 | [payer]
38 | )
39 | await connection.sendTransaction(tx)
40 | return lookupTable
41 | }
42 |
43 | /**
44 | *
45 | * Extends an Address Lookup Table by adding new addresses to the table
46 | *
47 | * @param connection Connection to Solana RPC
48 | * @param payer Transaction Fee Payer and Lookup Table Authority
49 | * @param lookupTable Address of the Lookup Table
50 | * @param addresses Addresses to add to the Lookup Table
51 | */
52 | export async function extendAddressLookupTable(
53 | connection: Connection,
54 | payer: Keypair,
55 | lookupTable: PublicKey,
56 | addresses: PublicKey[]
57 | ): Promise {
58 | let extendLookupTableIx = AddressLookupTableProgram.extendLookupTable({
59 | addresses,
60 | authority: payer.publicKey,
61 | lookupTable,
62 | payer: payer.publicKey,
63 | })
64 | const tx = await buildTransactionV0(
65 | connection,
66 | [extendLookupTableIx],
67 | payer.publicKey,
68 | [payer]
69 | )
70 | await connection.sendTransaction(tx)
71 | }
72 |
73 | /**
74 | *
75 | * Get an Address Lookup Table account
76 | *
77 | * @param connection Connection to Solana RPC
78 | * @param lookupTablePubkey The address of the Address Lookup Table
79 | */
80 | export async function getAddressLookupTable(
81 | connection: Connection,
82 | lookupTablePubkey: PublicKey
83 | ): Promise {
84 | return connection
85 | .getAddressLookupTable(lookupTablePubkey)
86 | .then((res) => res.value)
87 | }
88 |
89 | /**
90 | *
91 | * Print the contents of an Address Lookup Table
92 | *
93 | * @param connection Connection to Solana RPC
94 | * @param lookupTablePubkey The address of the Address Lookup Table
95 | */
96 | export async function printAddressLookupTable(
97 | connection: Connection,
98 | lookupTablePubkey: PublicKey
99 | ): Promise {
100 | // Lookup Table fetching can lag if this sleep isn't here
101 | await sleepSeconds(2)
102 | const lookupTableAccount = await getAddressLookupTable(
103 | connection,
104 | lookupTablePubkey
105 | )
106 | console.log(`Lookup Table: ${lookupTablePubkey}`)
107 | for (let i = 0; i < lookupTableAccount.state.addresses.length; i++) {
108 | const address = lookupTableAccount.state.addresses[i]
109 | console.log(
110 | ` Index: ${i
111 | .toString()
112 | .padEnd(2)} Address: ${address.toBase58()}`
113 | )
114 | }
115 | }
116 |
117 | /**
118 | *
119 | * Builds a transaction using the V0 format
120 | *
121 | * @param connection Connection to Solana RPC
122 | * @param instructions Instructions to send
123 | * @param payer Transaction Fee Payer
124 | * @param signers All required signers, in order
125 | * @returns The transaction v0
126 | */
127 | export async function buildTransactionV0(
128 | connection: Connection,
129 | instructions: TransactionInstruction[],
130 | payer: PublicKey,
131 | signers: Keypair[]
132 | ): Promise {
133 | let blockhash = await connection
134 | .getLatestBlockhash()
135 | .then((res) => res.blockhash)
136 | const messageV0 = new TransactionMessage({
137 | payerKey: payer,
138 | recentBlockhash: blockhash,
139 | instructions,
140 | }).compileToV0Message()
141 | const tx = new VersionedTransaction(messageV0)
142 | signers.forEach((s) => tx.sign([s]))
143 | return tx
144 | }
145 |
146 | /**
147 | *
148 | * Builds a transaction using the V0 format
149 | * using an Address Lookup Table
150 | *
151 | * @param connection Connection to Solana RPC
152 | * @param instructions Instructions to send
153 | * @param payer Transaction Fee Payer
154 | * @param signers All required signers, in order
155 | * @param lookupTablePubkey The address of the Address Lookup Table to use
156 | * @returns The transaction v0
157 | */
158 | export async function buildTransactionV0WithLookupTable(
159 | connection: Connection,
160 | instructions: TransactionInstruction[],
161 | payer: PublicKey,
162 | signers: Keypair[],
163 | lookupTablePubkey: PublicKey
164 | ): Promise {
165 | // Lookup Table fetching can lag if this sleep isn't here
166 | await sleepSeconds(2)
167 | const lookupTableAccount = await getAddressLookupTable(
168 | connection,
169 | lookupTablePubkey
170 | )
171 | let blockhash = await connection
172 | .getLatestBlockhash()
173 | .then((res) => res.blockhash)
174 | // Compile V0 Message with the Lookup Table
175 | const messageV0 = new TransactionMessage({
176 | payerKey: payer,
177 | recentBlockhash: blockhash,
178 | instructions,
179 | }).compileToV0Message([lookupTableAccount])
180 | const tx = new VersionedTransaction(messageV0)
181 | signers.forEach((s) => tx.sign([s]))
182 | return tx
183 | }
184 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "types": ["mocha", "chai"],
4 | "typeRoots": ["./node_modules/@types"],
5 | "lib": ["es2015"],
6 | "module": "commonjs",
7 | "target": "es2016",
8 | "esModuleInterop": true,
9 | "resolveJsonModule": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------