diff --git a/package.json b/package.json index e2e0736f1..46b88ded0 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@project-serum/serum": "0.13.65", "@pythnetwork/client": "~2.14.0", "@solana/spl-token": "0.3.7", - "@solana/web3.js": "^1.73.2", + "@solana/web3.js": "^1.78.2", "@switchboard-xyz/sbv2-lite": "^0.1.6", "big.js": "^6.1.1", "binance-api-node": "^0.12.0", diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 84cef99dd..9957714dd 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -26,6 +26,7 @@ import { SystemProgram, TransactionInstruction, TransactionSignature, + RecentPrioritizationFees, } from '@solana/web3.js'; import bs58 from 'bs58'; import chunk from 'lodash/chunk'; @@ -68,7 +69,12 @@ import { buildIxGate, TokenRegisterParams, } from './clientIxParamBuilder'; -import { MANGO_V4_ID, OPENBOOK_PROGRAM_ID, RUST_U64_MAX } from './constants'; +import { + MANGO_V4_ID, + MAX_RECENT_PRIORITY_FEE_ACCOUNTS, + OPENBOOK_PROGRAM_ID, + RUST_U64_MAX, +} from './constants'; import { Id } from './ids'; import { IDL, MangoV4 } from './mango_v4'; import { I80F48 } from './numbers/I80F48'; @@ -97,6 +103,7 @@ export type MangoClientOptions = { idsSource?: IdsSource; postSendTxCallback?: ({ txid }: { txid: string }) => void; prioritizationFee?: number; + estimateFee?: boolean; txConfirmationCommitment?: Commitment; openbookFeesToDao?: boolean; }; @@ -105,6 +112,7 @@ export class MangoClient { private idsSource: IdsSource; private postSendTxCallback?: ({ txid }) => void; private prioritizationFee: number; + private estimateFee: boolean; private txConfirmationCommitment: Commitment; private openbookFeesToDao: boolean; @@ -116,6 +124,7 @@ export class MangoClient { ) { this.idsSource = opts?.idsSource || 'get-program-accounts'; this.prioritizationFee = opts?.prioritizationFee || 0; + this.estimateFee = opts?.estimateFee || false; this.postSendTxCallback = opts?.postSendTxCallback; this.openbookFeesToDao = opts?.openbookFeesToDao ?? true; this.txConfirmationCommitment = @@ -140,13 +149,21 @@ export class MangoClient { ixs: TransactionInstruction[], opts: any = {}, ): Promise { + let prioritizationFee: number; + if (opts.prioritizationFee) { + prioritizationFee = opts.prioritizationFee; + } else if (this.estimateFee) { + prioritizationFee = await this.estimatePrioritizationFee(ixs); + } else { + prioritizationFee = this.prioritizationFee; + } return await sendTransaction( this.program.provider as AnchorProvider, ixs, opts.alts ?? [], { postSendTxCallback: this.postSendTxCallback, - prioritizationFee: this.prioritizationFee, + prioritizationFee, txConfirmationCommitment: this.txConfirmationCommitment, ...opts, }, @@ -4193,4 +4210,55 @@ export class MangoClient { transactionInstructions, ); } + + /** + * Returns an estimate of a prioritization fee for a set of instructions. + * + * The estimate is based on the exponential moving average of the prioritization fees of accounts that will be involved in the transaction. + * + * @param ixs - the instructions that make up the transaction + * @returns prioritizationFeeEstimate -- in microLamports + */ + public async estimatePrioritizationFee( + ixs: TransactionInstruction[], + ): Promise { + const accounts = [ + ...new Set(ixs.map((x) => x.keys.map((k) => k.pubkey)).flat()), + ].slice(0, MAX_RECENT_PRIORITY_FEE_ACCOUNTS); + const priorityFees = await this.connection.getRecentPrioritizationFees({ + lockedWritableAccounts: accounts, + }); + + // get max priority fee per slot (and sort by slot from old to new) + const flatFees = priorityFees.flat(); + const maxFeeBySlot = flatFees.reduce( + ( + acc: Record, + fee: RecentPrioritizationFees, + ) => { + if ( + !acc[fee.slot] || + fee.prioritizationFee > acc[fee.slot].prioritizationFee + ) { + acc[fee.slot] = fee; + } + return acc; + }, + {}, + ); + const maximumFees = Object.values(maxFeeBySlot).sort( + (a, b) => a.slot - b.slot, + ); + + // take the EMA + const smoothingFactor = 2 / (maximumFees.length + 1); + let ema = maximumFees[0].prioritizationFee; + for (let i = 1; i < maximumFees.length; i++) { + ema = + maximumFees[i].prioritizationFee * smoothingFactor + + ema * (1 - smoothingFactor); + } + + return Math.ceil(1.2 * ema); + } } diff --git a/ts/client/src/constants/index.ts b/ts/client/src/constants/index.ts index 1d8f39ea4..11e4ec270 100644 --- a/ts/client/src/constants/index.ts +++ b/ts/client/src/constants/index.ts @@ -25,3 +25,4 @@ export const MANGO_V4_ID = { export const USDC_MINT = new PublicKey( 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', ); +export const MAX_RECENT_PRIORITY_FEE_ACCOUNTS = 128; diff --git a/yarn.lock b/yarn.lock index f126a9eea..c0dbe0b0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -37,6 +37,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.22.6": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" + integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== + dependencies: + regenerator-runtime "^0.14.0" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -171,11 +178,23 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@noble/curves@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" + "@noble/ed25519@^1.7.0": version "1.7.3" resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123" integrity sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ== +"@noble/hashes@1.3.1", "@noble/hashes@^1.3.0": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + "@noble/hashes@^1.1.2": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" @@ -413,7 +432,7 @@ rpc-websockets "^7.5.0" superstruct "^0.14.2" -"@solana/web3.js@^1.68.0", "@solana/web3.js@^1.73.2": +"@solana/web3.js@^1.68.0": version "1.73.2" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.73.2.tgz#4b30cd402b35733dae3a7d0b638be26a7742b395" integrity sha512-9WACF8W4Nstj7xiDw3Oom22QmrhBh0VyZyZ7JvvG3gOxLWLlX3hvm5nPVJOGcCE/9fFavBbCUb5A6CIuvMGdoA== @@ -435,6 +454,27 @@ rpc-websockets "^7.5.0" superstruct "^0.14.2" +"@solana/web3.js@^1.78.2": + version "1.78.3" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.78.3.tgz#1214f17f51612e403c4dc7844c7a9694938bfbab" + integrity sha512-qhpnyIlrj/4Czw1dBFZK6KgZBk5FwuJhvMl0C7m94jhl90yDC8b6w4svKwPjhB+OOrdQAzHyRp0+ocEs/Liw7w== + dependencies: + "@babel/runtime" "^7.22.6" + "@noble/curves" "^1.0.0" + "@noble/hashes" "^1.3.0" + "@solana/buffer-layout" "^4.0.0" + agentkeepalive "^4.3.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.0" + node-fetch "^2.6.12" + rpc-websockets "^7.5.1" + superstruct "^0.14.2" + "@switchboard-xyz/sbv2-lite@^0.1.6": version "0.1.6" resolved "https://registry.yarnpkg.com/@switchboard-xyz/sbv2-lite/-/sbv2-lite-0.1.6.tgz#dc3fbb5b3b028dbd3c688b991bcc48a670131ddb" @@ -672,6 +712,13 @@ agentkeepalive@^4.2.1: depd "^1.1.2" humanize-ms "^1.2.1" +agentkeepalive@^4.3.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" @@ -858,7 +905,7 @@ bn.js@^4.11.9: resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== -bn.js@^5.0.0, bn.js@^5.1.0, bn.js@^5.1.2, bn.js@^5.1.3, bn.js@^5.2.0: +bn.js@^5.0.0, bn.js@^5.1.0, bn.js@^5.1.2, bn.js@^5.1.3, bn.js@^5.2.0, bn.js@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== @@ -1754,6 +1801,24 @@ jayson@^3.4.4: uuid "^8.3.2" ws "^7.4.5" +jayson@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.0.tgz#60dc946a85197317f2b1439d672a8b0a99cea2f9" + integrity sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A== + dependencies: + "@types/connect" "^3.4.33" + "@types/node" "^12.12.54" + "@types/ws" "^7.4.4" + JSONStream "^1.3.5" + commander "^2.20.3" + delay "^5.0.0" + es6-promisify "^5.0.0" + eyes "^0.1.8" + isomorphic-ws "^4.0.1" + json-stringify-safe "^5.0.1" + uuid "^8.3.2" + ws "^7.4.5" + js-sha256@^0.9.0: version "0.9.0" resolved "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz" @@ -2071,7 +2136,7 @@ node-addon-api@^2.0.0: resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== -node-fetch@2, node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7, "node-fetch@npm:@blockworks-foundation/node-fetch@2.6.11": +node-fetch@2, node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, "node-fetch@npm:@blockworks-foundation/node-fetch@2.6.11": version "2.6.11" resolved "https://registry.yarnpkg.com/@blockworks-foundation/node-fetch/-/node-fetch-2.6.11.tgz#fb536ef0e6a960e7b7993f3c1d3b3bba9bdfbc56" integrity sha512-HeDTxpIypSR4qCoqgUXGr8YL4OG1z7BbV4VhQ9iQs+pt2wV3MtqO+sQk2vXK3WDKu5C6BsbGmWE22BmIrcuOOw== @@ -2241,6 +2306,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + regexpp@^3.1.0, regexpp@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz"