@exponent-labs/exponent-sdk 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/client/vaults/index.d.ts +2 -0
- package/build/client/vaults/index.js +2 -0
- package/build/client/vaults/index.js.map +1 -1
- package/build/client/vaults/types/index.d.ts +2 -0
- package/build/client/vaults/types/index.js +2 -0
- package/build/client/vaults/types/index.js.map +1 -1
- package/build/client/vaults/types/kaminoFarmEntry.d.ts +15 -0
- package/build/client/vaults/types/kaminoFarmEntry.js +17 -0
- package/build/client/vaults/types/kaminoFarmEntry.js.map +1 -0
- package/build/client/vaults/types/kaminoObligationEntry.d.ts +21 -4
- package/build/client/vaults/types/kaminoObligationEntry.js +2 -1
- package/build/client/vaults/types/kaminoObligationEntry.js.map +1 -1
- package/build/client/vaults/types/positionUpdate.d.ts +9 -0
- package/build/client/vaults/types/positionUpdate.js +23 -0
- package/build/client/vaults/types/positionUpdate.js.map +1 -1
- package/build/client/vaults/types/proposalAction.js +0 -3
- package/build/client/vaults/types/proposalAction.js.map +1 -1
- package/build/client/vaults/types/reserveFarmMapping.d.ts +19 -0
- package/build/client/vaults/types/reserveFarmMapping.js +18 -0
- package/build/client/vaults/types/reserveFarmMapping.js.map +1 -0
- package/build/client/vaults/types/strategyPosition.d.ts +5 -0
- package/build/client/vaults/types/strategyPosition.js +5 -0
- package/build/client/vaults/types/strategyPosition.js.map +1 -1
- package/build/exponentVaults/aumCalculator.d.ts +25 -4
- package/build/exponentVaults/aumCalculator.js +236 -15
- package/build/exponentVaults/aumCalculator.js.map +1 -1
- package/build/exponentVaults/fetcher.d.ts +52 -0
- package/build/exponentVaults/fetcher.js +199 -0
- package/build/exponentVaults/fetcher.js.map +1 -0
- package/build/exponentVaults/index.d.ts +10 -9
- package/build/exponentVaults/index.js +26 -8
- package/build/exponentVaults/index.js.map +1 -1
- package/build/exponentVaults/kamino-farms.d.ts +144 -0
- package/build/exponentVaults/kamino-farms.js +396 -0
- package/build/exponentVaults/kamino-farms.js.map +1 -0
- package/build/exponentVaults/loopscale/client.d.ts +240 -0
- package/build/exponentVaults/loopscale/client.js +590 -0
- package/build/exponentVaults/loopscale/client.js.map +1 -0
- package/build/exponentVaults/loopscale/client.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/client.test.js +183 -0
- package/build/exponentVaults/loopscale/client.test.js.map +1 -0
- package/build/exponentVaults/loopscale/helpers.d.ts +29 -0
- package/build/exponentVaults/loopscale/helpers.js +119 -0
- package/build/exponentVaults/loopscale/helpers.js.map +1 -0
- package/build/exponentVaults/loopscale/index.d.ts +3 -0
- package/build/exponentVaults/loopscale/index.js +12 -0
- package/build/exponentVaults/loopscale/index.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.d.ts +13 -0
- package/build/exponentVaults/loopscale/prepared-transactions.js +271 -0
- package/build/exponentVaults/loopscale/prepared-transactions.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.js +400 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-types.d.ts +62 -0
- package/build/exponentVaults/loopscale/prepared-types.js +3 -0
- package/build/exponentVaults/loopscale/prepared-types.js.map +1 -0
- package/build/exponentVaults/loopscale/response-plan.d.ts +69 -0
- package/build/exponentVaults/loopscale/response-plan.js +141 -0
- package/build/exponentVaults/loopscale/response-plan.js.map +1 -0
- package/build/exponentVaults/loopscale/response-plan.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/response-plan.test.js +139 -0
- package/build/exponentVaults/loopscale/response-plan.test.js.map +1 -0
- package/build/exponentVaults/loopscale/send-plan.d.ts +75 -0
- package/build/exponentVaults/loopscale/send-plan.js +235 -0
- package/build/exponentVaults/loopscale/send-plan.js.map +1 -0
- package/build/exponentVaults/loopscale/types.d.ts +443 -0
- package/build/exponentVaults/loopscale/types.js +3 -0
- package/build/exponentVaults/loopscale/types.js.map +1 -0
- package/build/exponentVaults/loopscale-client.d.ts +113 -524
- package/build/exponentVaults/loopscale-client.js +296 -539
- package/build/exponentVaults/loopscale-client.js.map +1 -1
- package/build/exponentVaults/loopscale-client.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-client.test.js +162 -0
- package/build/exponentVaults/loopscale-client.test.js.map +1 -0
- package/build/exponentVaults/loopscale-client.types.d.ts +425 -0
- package/build/exponentVaults/loopscale-client.types.js +3 -0
- package/build/exponentVaults/loopscale-client.types.js.map +1 -0
- package/build/exponentVaults/loopscale-execution.d.ts +125 -0
- package/build/exponentVaults/loopscale-execution.js +341 -0
- package/build/exponentVaults/loopscale-execution.js.map +1 -0
- package/build/exponentVaults/loopscale-execution.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-execution.test.js +139 -0
- package/build/exponentVaults/loopscale-execution.test.js.map +1 -0
- package/build/exponentVaults/loopscale-vault.d.ts +115 -0
- package/build/exponentVaults/loopscale-vault.js +275 -0
- package/build/exponentVaults/loopscale-vault.js.map +1 -0
- package/build/exponentVaults/loopscale-vault.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-vault.test.js +102 -0
- package/build/exponentVaults/loopscale-vault.test.js.map +1 -0
- package/build/exponentVaults/policyBuilders.d.ts +62 -0
- package/build/exponentVaults/policyBuilders.js +119 -2
- package/build/exponentVaults/policyBuilders.js.map +1 -1
- package/build/exponentVaults/pricePathResolver.d.ts +45 -0
- package/build/exponentVaults/pricePathResolver.js +198 -0
- package/build/exponentVaults/pricePathResolver.js.map +1 -0
- package/build/exponentVaults/pricePathResolver.test.d.ts +1 -0
- package/build/exponentVaults/pricePathResolver.test.js +369 -0
- package/build/exponentVaults/pricePathResolver.test.js.map +1 -0
- package/build/exponentVaults/syncTransaction.js +4 -1
- package/build/exponentVaults/syncTransaction.js.map +1 -1
- package/build/exponentVaults/titan-quote.js +170 -36
- package/build/exponentVaults/titan-quote.js.map +1 -1
- package/build/exponentVaults/vault-instruction-types.d.ts +363 -0
- package/build/exponentVaults/vault-instruction-types.js +128 -0
- package/build/exponentVaults/vault-instruction-types.js.map +1 -0
- package/build/exponentVaults/vault-interaction.d.ts +203 -343
- package/build/exponentVaults/vault-interaction.js +1894 -426
- package/build/exponentVaults/vault-interaction.js.map +1 -1
- package/build/exponentVaults/vault-interaction.kamino-vault.test.d.ts +1 -0
- package/build/exponentVaults/vault-interaction.kamino-vault.test.js +143 -0
- package/build/exponentVaults/vault-interaction.kamino-vault.test.js.map +1 -0
- package/build/exponentVaults/vault.d.ts +51 -2
- package/build/exponentVaults/vault.js +324 -48
- package/build/exponentVaults/vault.js.map +1 -1
- package/build/exponentVaults/vaultTransactionBuilder.d.ts +100 -134
- package/build/exponentVaults/vaultTransactionBuilder.js +383 -285
- package/build/exponentVaults/vaultTransactionBuilder.js.map +1 -1
- package/build/exponentVaults/vaultTransactionBuilder.test.d.ts +1 -0
- package/build/exponentVaults/vaultTransactionBuilder.test.js +297 -0
- package/build/exponentVaults/vaultTransactionBuilder.test.js.map +1 -0
- package/build/marketThree.d.ts +6 -2
- package/build/marketThree.js +10 -8
- package/build/marketThree.js.map +1 -1
- package/package.json +34 -32
- package/src/client/vaults/index.ts +2 -0
- package/src/client/vaults/types/index.ts +2 -0
- package/src/client/vaults/types/kaminoFarmEntry.ts +32 -0
- package/src/client/vaults/types/kaminoObligationEntry.ts +6 -3
- package/src/client/vaults/types/positionUpdate.ts +62 -0
- package/src/client/vaults/types/proposalAction.ts +0 -3
- package/src/client/vaults/types/reserveFarmMapping.ts +35 -0
- package/src/client/vaults/types/strategyPosition.ts +18 -1
- package/src/exponentVaults/aumCalculator.ts +353 -16
- package/src/exponentVaults/fetcher.ts +257 -0
- package/src/exponentVaults/index.ts +65 -40
- package/src/exponentVaults/kamino-farms.ts +538 -0
- package/src/exponentVaults/loopscale/client.ts +808 -0
- package/src/exponentVaults/loopscale/helpers.ts +172 -0
- package/src/exponentVaults/loopscale/index.ts +57 -0
- package/src/exponentVaults/loopscale/prepared-transactions.ts +435 -0
- package/src/exponentVaults/loopscale/prepared-types.ts +73 -0
- package/src/exponentVaults/loopscale/types.ts +466 -0
- package/src/exponentVaults/policyBuilders.ts +170 -0
- package/src/exponentVaults/pricePathResolver.test.ts +466 -0
- package/src/exponentVaults/pricePathResolver.ts +273 -0
- package/src/exponentVaults/syncTransaction.ts +6 -1
- package/src/exponentVaults/titan-quote.ts +231 -45
- package/src/exponentVaults/vault-instruction-types.ts +493 -0
- package/src/exponentVaults/vault-interaction.kamino-vault.test.ts +149 -0
- package/src/exponentVaults/vault-interaction.ts +2818 -799
- package/src/exponentVaults/vault.ts +474 -63
- package/src/exponentVaults/vaultTransactionBuilder.test.ts +349 -0
- package/src/exponentVaults/vaultTransactionBuilder.ts +581 -433
- package/src/marketThree.ts +14 -6
- package/src/exponentVaults/loopscale-client.ts +0 -1373
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AddressLookupTableProgram,
|
|
2
3
|
Connection,
|
|
3
4
|
PublicKey,
|
|
4
5
|
TransactionInstruction,
|
|
5
6
|
TransactionMessage,
|
|
6
7
|
VersionedTransaction,
|
|
7
8
|
AddressLookupTableAccount,
|
|
8
|
-
AddressLookupTableProgram,
|
|
9
9
|
ComputeBudgetProgram,
|
|
10
|
-
SystemProgram,
|
|
11
10
|
} from "@solana/web3.js"
|
|
12
11
|
import type * as web3 from "@solana/web3.js"
|
|
13
12
|
import type { ExponentPrices } from "@exponent-labs/exponent-vaults-fetcher"
|
|
14
13
|
import type { ExponentVault } from "./vault"
|
|
15
|
-
import { LoopscaleClient } from "./loopscale
|
|
16
|
-
import {
|
|
14
|
+
import { LoopscaleClient } from "./loopscale"
|
|
15
|
+
import type { LoopscaleTransactionResponse } from "./loopscale"
|
|
16
|
+
import {
|
|
17
|
+
buildSetupStatePriceRefreshInstructions,
|
|
18
|
+
createVaultSyncTransactions,
|
|
19
|
+
createStrategySetupContext,
|
|
20
|
+
type VaultInstruction,
|
|
21
|
+
} from "./vault-interaction"
|
|
17
22
|
import { SQUADS_PROGRAM_ID } from "./syncTransaction"
|
|
23
|
+
import { LOCAL_ENV } from "../environment"
|
|
24
|
+
|
|
25
|
+
const SCOPE_PROGRAM_ID = new PublicKey("HFn8GnPADiny6XqUoWE8uRPPxb29ikn4yTuPa9MF2fWJ")
|
|
18
26
|
|
|
19
27
|
// ============================================================================
|
|
20
28
|
// Types
|
|
@@ -46,6 +54,20 @@ export interface StepOptions {
|
|
|
46
54
|
priorityFee?: number
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
/** Label-only options for raw Loopscale response steps. */
|
|
58
|
+
export interface LoopscaleStepOptions {
|
|
59
|
+
/** Human-readable label for this Loopscale step. Auto-generated if omitted. */
|
|
60
|
+
label?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Optional Loopscale client config used by the builder for preparation and MPC co-signing. */
|
|
64
|
+
export interface VaultTransactionBuilderLoopscaleConfig {
|
|
65
|
+
/** Override the Loopscale API base URL. */
|
|
66
|
+
baseUrl?: string
|
|
67
|
+
/** Enable verbose Loopscale request/response logging. */
|
|
68
|
+
debug?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
/**
|
|
50
72
|
* Configuration for creating a {@link VaultTransactionBuilder}.
|
|
51
73
|
*
|
|
@@ -55,8 +77,8 @@ export interface StepOptions {
|
|
|
55
77
|
* vault, // loaded ExponentVault instance
|
|
56
78
|
* connection, // Solana RPC connection
|
|
57
79
|
* signer: manager.publicKey, // strategy manager's public key
|
|
58
|
-
*
|
|
59
|
-
* pricesAccount, //
|
|
80
|
+
* autoManagePositions: true, // optional, defaults to true
|
|
81
|
+
* pricesAccount, // optional prefetched ExponentPrices snapshot
|
|
60
82
|
* })
|
|
61
83
|
* ```
|
|
62
84
|
*/
|
|
@@ -67,57 +89,55 @@ export interface VaultTransactionBuilderConfig {
|
|
|
67
89
|
connection: Connection
|
|
68
90
|
/** The strategy manager's public key. Used as the signer for policy matching and as the fee payer. */
|
|
69
91
|
signer: PublicKey
|
|
70
|
-
/**
|
|
71
|
-
vaultPda
|
|
92
|
+
/** Optional vault PDA override. Defaults to `vault.state.squadsVault`. */
|
|
93
|
+
vaultPda?: PublicKey
|
|
94
|
+
/** Optional extra lookup tables to include alongside the vault-managed tables. */
|
|
95
|
+
lookupTableAddresses?: PublicKey[]
|
|
72
96
|
/**
|
|
73
|
-
* Fetched `ExponentPrices` account snapshot.
|
|
97
|
+
* Fetched `ExponentPrices` account snapshot (optional).
|
|
74
98
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* Fetch this via `ExponentVaultsFetcher.fetchPricesDeserialized()` before creating the builder.
|
|
99
|
+
* When provided, the builder reuses this snapshot to assemble same-transaction
|
|
100
|
+
* `UpdatePrice` instructions for any setup or sync transaction that can hit
|
|
101
|
+
* AUM recalculation. When omitted, the builder fetches the current prices
|
|
102
|
+
* account automatically.
|
|
80
103
|
*/
|
|
81
|
-
pricesAccount
|
|
104
|
+
pricesAccount?: ExponentPrices
|
|
82
105
|
/** Default compute unit limit for all steps (default: 1,400,000). Override per-step via {@link StepOptions}. */
|
|
83
106
|
computeUnitLimit?: number
|
|
84
107
|
/** Default heap frame allocation in bytes for all steps (default: 262,144 = 256 KB). Override per-step via {@link StepOptions}. */
|
|
85
108
|
heapFrameBytes?: number
|
|
109
|
+
/** Automatically manage new Kamino/CLMM/yield position tracking for manager flows. Defaults to `true`. */
|
|
110
|
+
autoManagePositions?: boolean
|
|
86
111
|
/** Squads program ID override. Defaults to the standard Squads V4 program. */
|
|
87
112
|
squadsProgram?: PublicKey
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
*
|
|
91
|
-
* The price refresh tx runs oracle update instructions which are typically lighter
|
|
92
|
-
* than sync transactions. You can lower the CU limit here to save on fees, or
|
|
93
|
-
* set a priority fee to ensure the refresh lands quickly.
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
* ```ts
|
|
97
|
-
* VaultTransactionBuilder.create({
|
|
98
|
-
* // ...
|
|
99
|
-
* priceRefreshOptions: { computeUnitLimit: 600_000, priorityFee: 100 },
|
|
100
|
-
* })
|
|
101
|
-
* ```
|
|
102
|
-
*/
|
|
103
|
-
priceRefreshOptions?: StepOptions
|
|
113
|
+
/** Optional Loopscale client config used when preparing and co-signing Loopscale transactions. */
|
|
114
|
+
loopscale?: VaultTransactionBuilderLoopscaleConfig
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
type StepConfig =
|
|
118
|
+
| {
|
|
119
|
+
kind: "instructions"
|
|
120
|
+
label: string
|
|
121
|
+
instructions: VaultInstruction[]
|
|
122
|
+
options: StepOptions
|
|
123
|
+
}
|
|
124
|
+
| {
|
|
125
|
+
kind: "loopscale-response"
|
|
126
|
+
label: string
|
|
127
|
+
response: LoopscaleTransactionResponse
|
|
128
|
+
options: LoopscaleStepOptions
|
|
129
|
+
}
|
|
112
130
|
|
|
113
131
|
/**
|
|
114
132
|
* A group of instructions that will be assembled into a single VersionedTransaction.
|
|
115
133
|
*
|
|
116
|
-
* The builder produces an ordered array of TransactionSets:
|
|
117
|
-
*
|
|
134
|
+
* The builder produces an ordered array of TransactionSets: optional setup
|
|
135
|
+
* transactions, then one sync transaction per step. Same-transaction Exponent
|
|
136
|
+
* price refreshes are injected directly into any set that can hit AUM
|
|
137
|
+
* recalculation.
|
|
118
138
|
*/
|
|
119
139
|
export interface TransactionSet {
|
|
120
|
-
/** Human-readable label identifying this transaction (e.g. "
|
|
140
|
+
/** Human-readable label identifying this transaction (e.g. "step-1", "step-1-setup"). */
|
|
121
141
|
label: string
|
|
122
142
|
/** All instructions in execution order, including compute budget instructions. */
|
|
123
143
|
instructions: TransactionInstruction[]
|
|
@@ -125,121 +145,73 @@ export interface TransactionSet {
|
|
|
125
145
|
signers: web3.Signer[]
|
|
126
146
|
/** Address lookup table addresses that should be loaded for this transaction's V0 message. */
|
|
127
147
|
addressLookupTableAddresses: PublicKey[]
|
|
148
|
+
/** Whether this transaction needs Loopscale MPC co-signing before broadcast. */
|
|
149
|
+
coSignWithLoopscale?: boolean
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
/**
|
|
131
|
-
* Options for {@link VaultTransactionBuildResult.
|
|
153
|
+
* Options for {@link VaultTransactionBuildResult.send}.
|
|
132
154
|
*/
|
|
133
|
-
export interface
|
|
134
|
-
/** Keypair signers. The first signer is used as the fee payer
|
|
155
|
+
export interface SendOptions {
|
|
156
|
+
/** Keypair signers. The first signer is used as the fee payer. */
|
|
135
157
|
signers: web3.Signer[]
|
|
136
|
-
/**
|
|
137
|
-
tipLamports: number
|
|
138
|
-
/** Commitment level for bundle confirmation (default: "confirmed"). */
|
|
158
|
+
/** Commitment level for transaction confirmation (default: "confirmed"). */
|
|
139
159
|
commitment?: web3.Commitment
|
|
140
|
-
/** Jito auth key for authenticated block engine endpoints. */
|
|
141
|
-
jitoAuthKey?: string
|
|
142
|
-
/**
|
|
143
|
-
* Custom send function that replaces the default Jito bundle send.
|
|
144
|
-
*
|
|
145
|
-
* Receives signed (and co-signed) VersionedTransactions and their labels.
|
|
146
|
-
* Must return an array of transaction signatures in the same order.
|
|
147
|
-
* Use this for Surfpool, local validators, or any non-Jito send flow.
|
|
148
|
-
*
|
|
149
|
-
* @example
|
|
150
|
-
* ```ts
|
|
151
|
-
* result.sendJitoBundle({
|
|
152
|
-
* signers: [manager],
|
|
153
|
-
* tipLamports: 0,
|
|
154
|
-
* customSend: async (txs, labels) => {
|
|
155
|
-
* return Promise.all(txs.map(tx => surfpool.sendTransaction(tx)))
|
|
156
|
-
* },
|
|
157
|
-
* })
|
|
158
|
-
* ```
|
|
159
|
-
*/
|
|
160
|
-
customSend?: (transactions: VersionedTransaction[], labels: string[]) => Promise<string[]>
|
|
161
160
|
}
|
|
162
161
|
|
|
163
|
-
/** Result from {@link VaultTransactionBuildResult.
|
|
164
|
-
export interface
|
|
162
|
+
/** Result from {@link VaultTransactionBuildResult.send}. */
|
|
163
|
+
export interface SendResult {
|
|
165
164
|
/** Transaction signatures in the same order as the TransactionSets. */
|
|
166
165
|
signatures: string[]
|
|
167
|
-
/**
|
|
168
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Non-null when an ephemeral address lookup table was created to fit all
|
|
168
|
+
* accounts into the transactions. The caller can deactivate and close it
|
|
169
|
+
* to reclaim rent when they no longer need it.
|
|
170
|
+
*
|
|
171
|
+
* Cleanup requires two steps (Solana constraint):
|
|
172
|
+
* 1. `AddressLookupTableProgram.deactivateLookupTable({ lookupTable: address, authority })`
|
|
173
|
+
* 2. After ~one epoch, `AddressLookupTableProgram.closeLookupTable({ lookupTable: address, authority, recipient: authority })`
|
|
174
|
+
*/
|
|
175
|
+
ephemeralAlt: { address: PublicKey; authority: PublicKey } | null
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
/**
|
|
172
179
|
* The result of calling {@link VaultTransactionBuilder.build}.
|
|
173
180
|
*
|
|
174
|
-
* Contains the assembled TransactionSets and
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* Price refresh and sync transactions must land in the same slot — use `sendJitoBundle()`
|
|
178
|
-
* for atomic same-slot execution via Jito, or `customSend` for testing environments.
|
|
181
|
+
* Contains the assembled TransactionSets and a `send()` method for sequential execution.
|
|
182
|
+
* Loopscale co-signing is handled automatically.
|
|
179
183
|
*
|
|
180
184
|
* @example
|
|
181
185
|
* ```ts
|
|
182
186
|
* const result = await builder.build()
|
|
183
187
|
*
|
|
184
188
|
* // Inspect
|
|
185
|
-
* console.log(result.labels) // ["
|
|
186
|
-
*
|
|
187
|
-
* // Send as Jito bundle
|
|
188
|
-
* await result.sendJitoBundle({ signers: [manager], tipLamports: 10_000 })
|
|
189
|
+
* console.log(result.labels) // ["step-1", "loopscale-step-2"]
|
|
189
190
|
*
|
|
190
|
-
* //
|
|
191
|
-
* await result.
|
|
192
|
-
* signers: [manager], tipLamports: 0,
|
|
193
|
-
* customSend: async (txs) => surfpool.sendAll(txs),
|
|
194
|
-
* })
|
|
191
|
+
* // Send sequentially
|
|
192
|
+
* await result.send({ signers: [manager] })
|
|
195
193
|
* ```
|
|
196
194
|
*/
|
|
197
195
|
export interface VaultTransactionBuildResult {
|
|
198
|
-
/** Ordered TransactionSets:
|
|
196
|
+
/** Ordered TransactionSets: setup + sync per step, with same-tx price refresh injected as needed. */
|
|
199
197
|
transactionSets: TransactionSet[]
|
|
200
198
|
/** Deduplicated address lookup table addresses across all TransactionSets. */
|
|
201
199
|
lookupTableAddresses: PublicKey[]
|
|
202
200
|
/** Labels for each TransactionSet, in order. */
|
|
203
201
|
labels: string[]
|
|
204
202
|
/**
|
|
205
|
-
*
|
|
203
|
+
* Send transactions sequentially via regular RPC.
|
|
206
204
|
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
205
|
+
* Each transaction is built, signed (and co-signed for Loopscale steps),
|
|
206
|
+
* sent via `connection.sendTransaction()`, and confirmed before proceeding
|
|
207
|
+
* to the next one. Suitable for most operations.
|
|
210
208
|
*
|
|
211
|
-
*
|
|
209
|
+
* **Note:** Does not guarantee atomicity — if a later transaction fails,
|
|
210
|
+
* earlier ones have already landed.
|
|
212
211
|
*/
|
|
213
|
-
|
|
212
|
+
send(options: SendOptions): Promise<SendResult>
|
|
214
213
|
}
|
|
215
214
|
|
|
216
|
-
// ============================================================================
|
|
217
|
-
// Jito constants
|
|
218
|
-
// ============================================================================
|
|
219
|
-
|
|
220
|
-
const JITO_TIP_ADDRESSES = [
|
|
221
|
-
new PublicKey("96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5"),
|
|
222
|
-
new PublicKey("HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe"),
|
|
223
|
-
new PublicKey("Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY"),
|
|
224
|
-
new PublicKey("ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49"),
|
|
225
|
-
new PublicKey("DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh"),
|
|
226
|
-
new PublicKey("ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt"),
|
|
227
|
-
new PublicKey("DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL"),
|
|
228
|
-
new PublicKey("3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT"),
|
|
229
|
-
]
|
|
230
|
-
|
|
231
|
-
const JITO_REGIONAL_URLS = [
|
|
232
|
-
"https://amsterdam.mainnet.block-engine.jito.wtf",
|
|
233
|
-
"https://frankfurt.mainnet.block-engine.jito.wtf",
|
|
234
|
-
"https://ny.mainnet.block-engine.jito.wtf",
|
|
235
|
-
"https://tokyo.mainnet.block-engine.jito.wtf",
|
|
236
|
-
]
|
|
237
|
-
|
|
238
|
-
const JITO_MAINNET_URL = "https://mainnet.block-engine.jito.wtf"
|
|
239
|
-
const JITO_BUNDLES_PATH = "/api/v1/bundles"
|
|
240
|
-
const JITO_MAX_RETRIES = 10
|
|
241
|
-
const JITO_RETRY_DELAY_MS = 1000
|
|
242
|
-
|
|
243
215
|
// ============================================================================
|
|
244
216
|
// VaultTransactionBuilder
|
|
245
217
|
// ============================================================================
|
|
@@ -248,42 +220,44 @@ const JITO_RETRY_DELAY_MS = 1000
|
|
|
248
220
|
* Orchestrates strategy vault transaction construction.
|
|
249
221
|
*
|
|
250
222
|
* Replaces the per-recipe boilerplate of manually assembling compute budgets,
|
|
251
|
-
*
|
|
252
|
-
*
|
|
223
|
+
* sync transaction wrapping, ALT gathering, setup transaction splitting, and
|
|
224
|
+
* forward-looking Kamino/CLMM/yield position tracking.
|
|
253
225
|
*
|
|
254
|
-
* **Lifecycle:** create → add actions → build →
|
|
226
|
+
* **Lifecycle:** create → add actions → build → send()
|
|
255
227
|
*
|
|
256
228
|
* @example Single action
|
|
257
229
|
* ```ts
|
|
258
230
|
* const result = await VaultTransactionBuilder
|
|
259
|
-
* .create({ vault, connection, signer: manager.publicKey
|
|
231
|
+
* .create({ vault, connection, signer: manager.publicKey })
|
|
260
232
|
* .addActions([clmmAction.buyPt({ market, ptOutMin, syInMax })])
|
|
261
233
|
* .build()
|
|
262
234
|
*
|
|
263
|
-
* await result.
|
|
235
|
+
* await result.send({ signers: [manager] })
|
|
264
236
|
* ```
|
|
265
237
|
*
|
|
266
238
|
* @example Multi-step — each addActions() becomes its own sync transaction
|
|
267
239
|
* ```ts
|
|
268
240
|
* const result = await VaultTransactionBuilder
|
|
269
|
-
* .create({ vault, connection, signer: manager.publicKey
|
|
241
|
+
* .create({ vault, connection, signer: manager.publicKey })
|
|
270
242
|
* .addActions([kaminoAction.deposit("MAIN", "USDC", amount)]) // sync tx 1
|
|
271
243
|
* .addActions([kaminoAction.borrow("MAIN", "SOL", amount)]) // sync tx 2
|
|
272
244
|
* .build()
|
|
273
245
|
*
|
|
274
|
-
* await result.
|
|
246
|
+
* await result.send({ signers: [manager] })
|
|
275
247
|
* ```
|
|
276
248
|
*
|
|
277
249
|
* @example Mixed Kamino + Loopscale — co-signing handled automatically
|
|
278
250
|
* ```ts
|
|
251
|
+
* const loopscaleDeposit = await loopscale.depositStrategy({ ... })
|
|
252
|
+
*
|
|
279
253
|
* const result = await VaultTransactionBuilder
|
|
280
|
-
* .create({ vault, connection, signer: manager.publicKey
|
|
254
|
+
* .create({ vault, connection, signer: manager.publicKey })
|
|
281
255
|
* .addActions([kaminoAction.deposit(...)]) // signed normally
|
|
282
|
-
* .
|
|
256
|
+
* .addLoopscaleResponse(loopscaleDeposit) // co-signed automatically
|
|
283
257
|
* .build()
|
|
284
258
|
*
|
|
285
|
-
* //
|
|
286
|
-
* await result.
|
|
259
|
+
* // send() auto-creates a LoopscaleClient for Loopscale co-signing
|
|
260
|
+
* await result.send({ signers: [manager] })
|
|
287
261
|
* ```
|
|
288
262
|
*/
|
|
289
263
|
export class VaultTransactionBuilder {
|
|
@@ -298,7 +272,7 @@ export class VaultTransactionBuilder {
|
|
|
298
272
|
/**
|
|
299
273
|
* Create a new builder instance.
|
|
300
274
|
*
|
|
301
|
-
* @param config - Builder configuration including the vault, connection,
|
|
275
|
+
* @param config - Builder configuration including the vault, connection, and signer.
|
|
302
276
|
* @returns A new {@link VaultTransactionBuilder} ready for action chaining.
|
|
303
277
|
*/
|
|
304
278
|
static create(config: VaultTransactionBuilderConfig): VaultTransactionBuilder {
|
|
@@ -331,30 +305,29 @@ export class VaultTransactionBuilder {
|
|
|
331
305
|
addActions(instructions: VaultInstruction[], options?: StepOptions): this {
|
|
332
306
|
this.stepCounter++
|
|
333
307
|
const label = options?.label ?? `step-${this.stepCounter}`
|
|
334
|
-
this.steps.push({ label, instructions: [...instructions],
|
|
308
|
+
this.steps.push({ kind: "instructions", label, instructions: [...instructions], options: options ?? {} })
|
|
335
309
|
return this
|
|
336
310
|
}
|
|
337
311
|
|
|
338
312
|
/**
|
|
339
|
-
* Add Loopscale
|
|
313
|
+
* Add a raw Loopscale response as a new step.
|
|
340
314
|
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
315
|
+
* The builder will flatten the response into manager-facing transactions,
|
|
316
|
+
* preserving Loopscale-signed segments while keeping local setup/top-level
|
|
317
|
+
* transactions outside the Loopscale signing domain.
|
|
344
318
|
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
319
|
+
* @param response - Raw Loopscale response returned by {@link LoopscaleClient}.
|
|
320
|
+
* Only `label` is supported for Loopscale steps. Compute overrides are
|
|
321
|
+
* preserved on the raw Loopscale response and are not applied by the
|
|
322
|
+
* builder.
|
|
349
323
|
*
|
|
350
|
-
* @param
|
|
351
|
-
* @param options - Optional per-step compute budget overrides and label.
|
|
324
|
+
* @param options - Optional step label.
|
|
352
325
|
* @returns `this` for chaining.
|
|
353
326
|
*/
|
|
354
|
-
|
|
327
|
+
addLoopscaleResponse(response: LoopscaleTransactionResponse, options?: LoopscaleStepOptions): this {
|
|
355
328
|
this.stepCounter++
|
|
356
329
|
const label = options?.label ?? `loopscale-step-${this.stepCounter}`
|
|
357
|
-
this.steps.push({
|
|
330
|
+
this.steps.push({ kind: "loopscale-response", label, response, options: options ?? {} })
|
|
358
331
|
return this
|
|
359
332
|
}
|
|
360
333
|
|
|
@@ -362,51 +335,145 @@ export class VaultTransactionBuilder {
|
|
|
362
335
|
* Build TransactionSets from the configured steps.
|
|
363
336
|
*
|
|
364
337
|
* Produces an ordered array of TransactionSets:
|
|
365
|
-
* 1. **
|
|
366
|
-
* 2. **
|
|
367
|
-
*
|
|
338
|
+
* 1. **Setup transactions** — token account creation / builder-managed setup (only if needed)
|
|
339
|
+
* 2. **Sync transactions** — one per step, wrapped in Squads sync transaction
|
|
340
|
+
*
|
|
341
|
+
* Same-transaction `UpdatePrice` instructions are injected directly into any
|
|
342
|
+
* setup or sync set that can hit AUM recalculation.
|
|
368
343
|
*
|
|
369
|
-
*
|
|
370
|
-
* Use `result.sendJitoBundle()` for atomic same-slot execution.
|
|
344
|
+
* Use `result.send()` for sequential execution.
|
|
371
345
|
*
|
|
372
|
-
* @returns A {@link VaultTransactionBuildResult} with TransactionSets and a
|
|
346
|
+
* @returns A {@link VaultTransactionBuildResult} with TransactionSets and a send method.
|
|
373
347
|
* @throws If no steps have been configured (call addActions first).
|
|
374
348
|
* @throws If any assembled transaction exceeds the 1232-byte Solana packet limit.
|
|
375
349
|
*/
|
|
376
350
|
async build(): Promise<VaultTransactionBuildResult> {
|
|
377
351
|
if (this.steps.length === 0) {
|
|
378
|
-
throw new Error("No steps configured. Call addActions() or
|
|
352
|
+
throw new Error("No steps configured. Call addActions() or addLoopscaleResponse() before build().")
|
|
379
353
|
}
|
|
380
354
|
|
|
381
|
-
const { vault, connection, signer,
|
|
355
|
+
const { vault, connection, signer, pricesAccount } = this.config
|
|
356
|
+
const vaultPda = this.config.vaultPda ?? vault.state.squadsVault
|
|
357
|
+
const extraLookupTableAddresses = this.config.lookupTableAddresses ?? []
|
|
358
|
+
const autoManagePositions = this.config.autoManagePositions ?? true
|
|
382
359
|
const squadsProgram = this.config.squadsProgram ?? SQUADS_PROGRAM_ID
|
|
383
360
|
const defaultCU = this.config.computeUnitLimit ?? 1_400_000
|
|
384
361
|
const defaultHeap = this.config.heapFrameBytes ?? 256 * 1024
|
|
385
362
|
|
|
386
|
-
|
|
387
|
-
const priceOpts = this.config.priceRefreshOptions ?? {}
|
|
388
|
-
const priceIxs = await vault.ixsUpdateStrategyVaultPrices(pricesAccount)
|
|
389
|
-
const priceSet: TransactionSet = {
|
|
390
|
-
label: priceOpts.label ?? "price-refresh",
|
|
391
|
-
instructions: buildComputeBudgetIxs(
|
|
392
|
-
priceOpts.computeUnitLimit ?? defaultCU,
|
|
393
|
-
priceOpts.heapFrameBytes ?? defaultHeap,
|
|
394
|
-
priceOpts.priorityFee,
|
|
395
|
-
).concat(priceIxs),
|
|
396
|
-
signers: [],
|
|
397
|
-
addressLookupTableAddresses: vault.state.addressLookupTable
|
|
398
|
-
? [vault.state.addressLookupTable]
|
|
399
|
-
: [],
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// 2. Per-step sync TransactionSets
|
|
403
|
-
const transactionSets: TransactionSet[] = [priceSet]
|
|
363
|
+
const transactionSets: TransactionSet[] = []
|
|
404
364
|
const allAltAddresses = new Set<string>(
|
|
405
|
-
|
|
365
|
+
[
|
|
366
|
+
...(vault.state.addressLookupTable ? [vault.state.addressLookupTable.toBase58()] : []),
|
|
367
|
+
...extraLookupTableAddresses.map((address) => address.toBase58()),
|
|
368
|
+
],
|
|
406
369
|
)
|
|
407
370
|
|
|
371
|
+
// Share a single setup context across all steps so that tracked accounts,
|
|
372
|
+
// positions, and orderbooks carry over — preventing duplicate setup txs.
|
|
373
|
+
const sharedSetupContext = createStrategySetupContext({
|
|
374
|
+
connection,
|
|
375
|
+
env: LOCAL_ENV,
|
|
376
|
+
owner: vaultPda,
|
|
377
|
+
signer,
|
|
378
|
+
vaultAddress: vault.selfAddress,
|
|
379
|
+
vaultPda,
|
|
380
|
+
accountIndex: 0,
|
|
381
|
+
squadsProgram,
|
|
382
|
+
autoManagePositions,
|
|
383
|
+
pricesAccount,
|
|
384
|
+
})
|
|
385
|
+
let loopscaleClient: LoopscaleClient | undefined
|
|
386
|
+
|
|
408
387
|
for (const step of this.steps) {
|
|
409
|
-
|
|
388
|
+
if (step.kind === "loopscale-response") {
|
|
389
|
+
loopscaleClient ??= createBuilderLoopscaleClient({
|
|
390
|
+
connection,
|
|
391
|
+
vaultPda,
|
|
392
|
+
loopscale: this.config.loopscale,
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
const preparedTransactions = await loopscaleClient.prepareVaultTransactions({
|
|
396
|
+
response: step.response,
|
|
397
|
+
context: {
|
|
398
|
+
connection,
|
|
399
|
+
signer,
|
|
400
|
+
vaultPda,
|
|
401
|
+
vaultAddress: vault.selfAddress,
|
|
402
|
+
squadsProgram,
|
|
403
|
+
},
|
|
404
|
+
})
|
|
405
|
+
const stepPriceRefreshIxs = await buildSetupStatePriceRefreshInstructions(sharedSetupContext)
|
|
406
|
+
|
|
407
|
+
for (const [preparedIndex, preparedTransaction] of preparedTransactions.entries()) {
|
|
408
|
+
const txLabel = preparedTransactions.length === 1
|
|
409
|
+
? step.label
|
|
410
|
+
: `${step.label}-${preparedIndex + 1}`
|
|
411
|
+
|
|
412
|
+
if (preparedTransaction.setupInstructions.length > 0) {
|
|
413
|
+
const setupAltAddresses = mergeAddressLookupTableAddresses(
|
|
414
|
+
extraLookupTableAddresses,
|
|
415
|
+
vault.state.addressLookupTable ? [vault.state.addressLookupTable] : [],
|
|
416
|
+
)
|
|
417
|
+
const setupSet: TransactionSet = {
|
|
418
|
+
label: `${txLabel}-setup`,
|
|
419
|
+
instructions: [
|
|
420
|
+
ComputeBudgetProgram.setComputeUnitLimit({ units: defaultCU }),
|
|
421
|
+
...preparedTransaction.setupInstructions,
|
|
422
|
+
],
|
|
423
|
+
signers: [],
|
|
424
|
+
addressLookupTableAddresses: setupAltAddresses,
|
|
425
|
+
coSignWithLoopscale: false,
|
|
426
|
+
}
|
|
427
|
+
if (touchesManagedVaultPrograms(setupSet.instructions, vault.programId, squadsProgram)) {
|
|
428
|
+
setupSet.instructions = insertAfterLeadingComputeBudgetAndScopeInstructions(
|
|
429
|
+
setupSet.instructions,
|
|
430
|
+
stepPriceRefreshIxs,
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
transactionSets.push(setupSet)
|
|
434
|
+
for (const alt of setupSet.addressLookupTableAddresses) {
|
|
435
|
+
allAltAddresses.add(alt.toBase58())
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const shouldInjectLocalLoopscalePriceRefresh = (
|
|
440
|
+
!preparedTransaction.requiresLoopscaleCoSign
|
|
441
|
+
&& touchesManagedVaultPrograms(preparedTransaction.instructions, vault.programId, squadsProgram)
|
|
442
|
+
)
|
|
443
|
+
const loopscaleSet: TransactionSet = {
|
|
444
|
+
label: txLabel,
|
|
445
|
+
instructions: preparedTransaction.instructions,
|
|
446
|
+
signers: preparedTransaction.signers,
|
|
447
|
+
addressLookupTableAddresses: shouldInjectLocalLoopscalePriceRefresh
|
|
448
|
+
? mergeAddressLookupTableAddresses(
|
|
449
|
+
preparedTransaction.addressLookupTableAddresses,
|
|
450
|
+
extraLookupTableAddresses,
|
|
451
|
+
vault.state.addressLookupTable ? [vault.state.addressLookupTable] : [],
|
|
452
|
+
)
|
|
453
|
+
: mergeAddressLookupTableAddresses(
|
|
454
|
+
preparedTransaction.addressLookupTableAddresses,
|
|
455
|
+
extraLookupTableAddresses,
|
|
456
|
+
),
|
|
457
|
+
coSignWithLoopscale: preparedTransaction.requiresLoopscaleCoSign,
|
|
458
|
+
}
|
|
459
|
+
if (shouldInjectLocalLoopscalePriceRefresh) {
|
|
460
|
+
loopscaleSet.instructions = insertAfterLeadingComputeBudgetAndScopeInstructions(
|
|
461
|
+
loopscaleSet.instructions,
|
|
462
|
+
stepPriceRefreshIxs,
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
transactionSets.push(loopscaleSet)
|
|
466
|
+
|
|
467
|
+
for (const alt of loopscaleSet.addressLookupTableAddresses) {
|
|
468
|
+
allAltAddresses.add(alt.toBase58())
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
continue
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const stepCU = step.options.computeUnitLimit ?? defaultCU
|
|
475
|
+
const stepHeap = step.options.heapFrameBytes ?? defaultHeap
|
|
476
|
+
const syncResults = await createVaultSyncTransactions({
|
|
410
477
|
instructions: step.instructions,
|
|
411
478
|
owner: vaultPda,
|
|
412
479
|
connection,
|
|
@@ -414,215 +481,219 @@ export class VaultTransactionBuilder {
|
|
|
414
481
|
signer,
|
|
415
482
|
vaultAddress: vault.selfAddress,
|
|
416
483
|
squadsProgram,
|
|
484
|
+
autoManagePositions,
|
|
485
|
+
setupContext: sharedSetupContext,
|
|
417
486
|
})
|
|
418
487
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
488
|
+
const stepPriceRefreshIxs = await buildSetupStatePriceRefreshInstructions(sharedSetupContext)
|
|
489
|
+
|
|
490
|
+
for (const [syncIndex, syncResult] of syncResults.entries()) {
|
|
491
|
+
const txLabel = syncResults.length === 1
|
|
492
|
+
? step.label
|
|
493
|
+
: `${step.label}-${syncIndex + 1}`
|
|
494
|
+
|
|
495
|
+
if (syncResult.setupInstructions.length > 0) {
|
|
496
|
+
const setupSet: TransactionSet = {
|
|
497
|
+
label: `${txLabel}-setup`,
|
|
498
|
+
instructions: [
|
|
499
|
+
ComputeBudgetProgram.setComputeUnitLimit({ units: defaultCU }),
|
|
500
|
+
...syncResult.setupInstructions,
|
|
501
|
+
],
|
|
502
|
+
signers: [],
|
|
503
|
+
addressLookupTableAddresses: mergeAddressLookupTableAddresses(
|
|
504
|
+
syncResult.addressLookupTableAddresses,
|
|
505
|
+
extraLookupTableAddresses,
|
|
506
|
+
vault.state.addressLookupTable ? [vault.state.addressLookupTable] : [],
|
|
507
|
+
),
|
|
508
|
+
coSignWithLoopscale: false,
|
|
509
|
+
}
|
|
510
|
+
if (shouldInjectPriceRefresh(setupSet, vault.programId, squadsProgram)) {
|
|
511
|
+
setupSet.instructions = insertAfterLeadingComputeBudgetAndScopeInstructions(
|
|
512
|
+
setupSet.instructions,
|
|
513
|
+
stepPriceRefreshIxs,
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
transactionSets.push(setupSet)
|
|
517
|
+
for (const alt of setupSet.addressLookupTableAddresses) {
|
|
518
|
+
allAltAddresses.add(alt.toBase58())
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const syncSet: TransactionSet = {
|
|
523
|
+
label: txLabel,
|
|
422
524
|
instructions: [
|
|
423
|
-
|
|
424
|
-
...syncResult.
|
|
525
|
+
...buildComputeBudgetIxs(stepCU, stepHeap, step.options.priorityFee),
|
|
526
|
+
...syncResult.preInstructions,
|
|
527
|
+
syncResult.instruction,
|
|
528
|
+
...syncResult.postInstructions,
|
|
425
529
|
],
|
|
426
|
-
signers:
|
|
427
|
-
addressLookupTableAddresses:
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
],
|
|
442
|
-
signers: syncResult.signers,
|
|
443
|
-
addressLookupTableAddresses: [
|
|
444
|
-
...syncResult.addressLookupTableAddresses,
|
|
445
|
-
...(vault.state.addressLookupTable ? [vault.state.addressLookupTable] : []),
|
|
446
|
-
],
|
|
447
|
-
}
|
|
448
|
-
transactionSets.push(syncSet)
|
|
530
|
+
signers: syncResult.signers,
|
|
531
|
+
addressLookupTableAddresses: mergeAddressLookupTableAddresses(
|
|
532
|
+
syncResult.addressLookupTableAddresses,
|
|
533
|
+
extraLookupTableAddresses,
|
|
534
|
+
vault.state.addressLookupTable ? [vault.state.addressLookupTable] : [],
|
|
535
|
+
),
|
|
536
|
+
coSignWithLoopscale: false,
|
|
537
|
+
}
|
|
538
|
+
if (shouldInjectPriceRefresh(syncSet, vault.programId, squadsProgram)) {
|
|
539
|
+
syncSet.instructions = insertAfterLeadingComputeBudgetAndScopeInstructions(
|
|
540
|
+
syncSet.instructions,
|
|
541
|
+
stepPriceRefreshIxs,
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
transactionSets.push(syncSet)
|
|
449
545
|
|
|
450
|
-
|
|
451
|
-
|
|
546
|
+
for (const alt of syncSet.addressLookupTableAddresses) {
|
|
547
|
+
allAltAddresses.add(alt.toBase58())
|
|
548
|
+
}
|
|
452
549
|
}
|
|
453
550
|
}
|
|
454
551
|
|
|
455
552
|
const lookupTableAddresses = [...allAltAddresses].map((a) => new PublicKey(a))
|
|
456
553
|
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
const testMsg = new TransactionMessage({
|
|
465
|
-
payerKey: signer,
|
|
466
|
-
recentBlockhash: "1".repeat(44),
|
|
467
|
-
instructions: txSet.instructions,
|
|
468
|
-
}).compileToV0Message(txAltAccounts)
|
|
469
|
-
const testTx = new VersionedTransaction(testMsg)
|
|
470
|
-
const serialized = testTx.serialize()
|
|
471
|
-
if (serialized.length > 1232) {
|
|
472
|
-
throw new Error(
|
|
473
|
-
`Transaction "${txSet.label}" is oversized (${serialized.length} bytes, limit 1232). ` +
|
|
474
|
-
`This usually means too many actions were packed into a single addActions() call. ` +
|
|
475
|
-
`Split them into separate addActions() calls so each gets its own sync transaction.`,
|
|
476
|
-
)
|
|
477
|
-
}
|
|
478
|
-
} catch (e) {
|
|
479
|
-
if (e instanceof Error && e.message.includes("oversized")) throw e
|
|
554
|
+
// Resolve the vault's persistent ALT to determine which account keys it
|
|
555
|
+
// already covers. Any keys NOT covered will need an ephemeral ALT.
|
|
556
|
+
const persistentAltAccounts = await resolveAltAccounts(connection, lookupTableAddresses)
|
|
557
|
+
const persistentAltKeys = new Set<string>()
|
|
558
|
+
for (const alt of persistentAltAccounts) {
|
|
559
|
+
for (const addr of alt.state.addresses) {
|
|
560
|
+
persistentAltKeys.add(addr.toBase58())
|
|
480
561
|
}
|
|
481
562
|
}
|
|
482
563
|
|
|
564
|
+
// Collect every unique account key referenced by any transaction set.
|
|
565
|
+
const allUniqueKeys = collectUniqueAccountKeys(transactionSets, signer)
|
|
566
|
+
const ephemeralAltEntries = allUniqueKeys.filter((k) => !persistentAltKeys.has(k.toBase58()))
|
|
567
|
+
|
|
568
|
+
// Validate transaction sizes. When an ephemeral ALT will be created,
|
|
569
|
+
// skip validation here — send() will validate after ALT activation.
|
|
570
|
+
if (ephemeralAltEntries.length === 0) {
|
|
571
|
+
validateTransactionSizes(transactionSets, persistentAltAccounts, signer)
|
|
572
|
+
}
|
|
573
|
+
|
|
483
574
|
const labels = transactionSets.map((ts) => ts.label)
|
|
484
575
|
|
|
485
|
-
// Capture state for
|
|
576
|
+
// Capture state for send() closure
|
|
486
577
|
const closedConnection = connection
|
|
487
578
|
const closedSigner = signer
|
|
579
|
+
const closedVaultPda = vaultPda
|
|
488
580
|
const closedLookupTableAddresses = lookupTableAddresses
|
|
489
|
-
const
|
|
581
|
+
const closedLoopscaleConfig = this.config.loopscale
|
|
490
582
|
|
|
491
583
|
return {
|
|
492
584
|
transactionSets,
|
|
493
585
|
lookupTableAddresses,
|
|
494
586
|
labels,
|
|
495
587
|
|
|
496
|
-
async
|
|
588
|
+
async send(options: SendOptions): Promise<SendResult> {
|
|
589
|
+
const commitment = options.commitment ?? "confirmed"
|
|
590
|
+
const payer = options.signers[0]
|
|
591
|
+
|
|
592
|
+
// ── Ephemeral ALT creation ───────────────────────────────────────
|
|
593
|
+
let ephemeralAlt: { address: PublicKey; authority: PublicKey } | null = null
|
|
594
|
+
let ephemeralAltAccount: AddressLookupTableAccount | undefined
|
|
595
|
+
|
|
596
|
+
if (ephemeralAltEntries.length > 0) {
|
|
597
|
+
const slot = await closedConnection.getSlot("processed")
|
|
598
|
+
const recentSlot = Math.max(slot - 1, 0)
|
|
599
|
+
const [createIx, altAddress] = AddressLookupTableProgram.createLookupTable({
|
|
600
|
+
authority: payer.publicKey,
|
|
601
|
+
payer: payer.publicKey,
|
|
602
|
+
recentSlot,
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
// Chunk extend instructions — each can hold ~20 addresses safely
|
|
606
|
+
const extendChunks = chunkArray(ephemeralAltEntries, 20)
|
|
607
|
+
const extendIxs = extendChunks.map((chunk) =>
|
|
608
|
+
AddressLookupTableProgram.extendLookupTable({
|
|
609
|
+
lookupTable: altAddress,
|
|
610
|
+
authority: payer.publicKey,
|
|
611
|
+
payer: payer.publicKey,
|
|
612
|
+
addresses: chunk,
|
|
613
|
+
}),
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
// Send create + first extend in one transaction
|
|
617
|
+
const altSetupIxs: TransactionInstruction[] = [createIx, extendIxs[0]!]
|
|
618
|
+
await sendAndConfirmTransaction(closedConnection, altSetupIxs, [payer], commitment)
|
|
619
|
+
|
|
620
|
+
// Send remaining extend chunks if any
|
|
621
|
+
for (let i = 1; i < extendIxs.length; i++) {
|
|
622
|
+
await sendAndConfirmTransaction(closedConnection, [extendIxs[i]!], [payer], commitment)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Wait for ALT entries to become active (usable in the slot AFTER extension)
|
|
626
|
+
await waitForSlotAdvance(closedConnection, slot, "processed")
|
|
627
|
+
|
|
628
|
+
// Resolve the ephemeral ALT account for use in transactions
|
|
629
|
+
const resolved = await closedConnection.getAddressLookupTable(altAddress)
|
|
630
|
+
if (!resolved.value) {
|
|
631
|
+
throw new Error(`Ephemeral ALT ${altAddress.toBase58()} not found after creation`)
|
|
632
|
+
}
|
|
633
|
+
ephemeralAltAccount = resolved.value
|
|
634
|
+
ephemeralAlt = { address: altAddress, authority: payer.publicKey }
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ── Resolve all ALT accounts (persistent + ephemeral) ────────────
|
|
497
638
|
const resolvedAltAccounts = await resolveAltAccounts(closedConnection, closedLookupTableAddresses)
|
|
498
|
-
|
|
639
|
+
if (ephemeralAltAccount) {
|
|
640
|
+
resolvedAltAccounts.push(ephemeralAltAccount)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// If we skipped build-time validation, validate now with the full ALT set
|
|
644
|
+
if (ephemeralAltEntries.length > 0) {
|
|
645
|
+
validateTransactionSizes(transactionSets, resolvedAltAccounts, closedSigner, ephemeralAltAccount)
|
|
646
|
+
}
|
|
499
647
|
|
|
500
|
-
// Auto-create LoopscaleClient if any step needs co-signing
|
|
501
|
-
const hasLoopscale =
|
|
648
|
+
// ── Auto-create LoopscaleClient if any step needs co-signing ─────
|
|
649
|
+
const hasLoopscale = transactionSets.some((txSet) => txSet.coSignWithLoopscale)
|
|
502
650
|
let loopscaleClient: LoopscaleClient | undefined
|
|
503
651
|
if (hasLoopscale) {
|
|
504
|
-
loopscaleClient =
|
|
652
|
+
loopscaleClient = createBuilderLoopscaleClient({
|
|
505
653
|
connection: closedConnection,
|
|
506
|
-
|
|
654
|
+
vaultPda: closedVaultPda,
|
|
655
|
+
loopscale: closedLoopscaleConfig,
|
|
507
656
|
})
|
|
508
657
|
}
|
|
509
658
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
const instructions = [...txSet.instructions]
|
|
516
|
-
if (isLast && options.tipLamports > 0) {
|
|
517
|
-
instructions.push(createJitoTipIx(options.tipLamports, options.signers[0].publicKey))
|
|
518
|
-
}
|
|
659
|
+
// ── Send transactions ────────────────────────────────────────────
|
|
660
|
+
const signatures: string[] = []
|
|
661
|
+
for (const txSet of transactionSets) {
|
|
662
|
+
const { blockhash, lastValidBlockHeight } = await closedConnection.getLatestBlockhash()
|
|
519
663
|
|
|
664
|
+
// Include the ephemeral ALT for every transaction set
|
|
520
665
|
const txAltAccounts = resolvedAltAccounts.filter((alt) =>
|
|
521
|
-
txSet.addressLookupTableAddresses.some((addr) => addr.equals(alt.key))
|
|
666
|
+
txSet.addressLookupTableAddresses.some((addr) => addr.equals(alt.key))
|
|
667
|
+
|| (ephemeralAltAccount && alt.key.equals(ephemeralAltAccount.key)),
|
|
522
668
|
)
|
|
523
669
|
|
|
524
670
|
const msg = new TransactionMessage({
|
|
525
|
-
payerKey:
|
|
671
|
+
payerKey: payer.publicKey,
|
|
526
672
|
recentBlockhash: blockhash,
|
|
527
|
-
instructions,
|
|
673
|
+
instructions: txSet.instructions,
|
|
528
674
|
}).compileToV0Message(txAltAccounts)
|
|
529
675
|
|
|
530
676
|
let tx = new VersionedTransaction(msg)
|
|
531
|
-
|
|
532
|
-
// Co-sign Loopscale steps, sign everything else normally
|
|
533
|
-
const step = closedSteps.find((s) => s.label === txSet.label)
|
|
534
|
-
if (step?.isLoopscale && loopscaleClient) {
|
|
677
|
+
if (txSet.coSignWithLoopscale && loopscaleClient) {
|
|
535
678
|
tx.sign([...options.signers, ...txSet.signers])
|
|
536
679
|
tx = await loopscaleClient.coSign(tx)
|
|
537
680
|
} else {
|
|
538
681
|
tx.sign([...options.signers, ...txSet.signers])
|
|
539
682
|
}
|
|
540
683
|
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Custom send escape hatch (for testing)
|
|
545
|
-
if (options.customSend) {
|
|
546
|
-
const sigs = await options.customSend(allTxs, labels)
|
|
547
|
-
return { signatures: sigs, bundleId: "" }
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const serialized = allTxs.map((tx) => Buffer.from(tx.serialize()))
|
|
551
|
-
const result = await sendJitoBundleRpc(serialized, options.jitoAuthKey)
|
|
552
|
-
|
|
553
|
-
const commitment = options.commitment ?? "confirmed"
|
|
554
|
-
if (result.signatures.length > 0) {
|
|
684
|
+
const sig = await closedConnection.sendTransaction(tx)
|
|
555
685
|
await closedConnection.confirmTransaction(
|
|
556
|
-
{ signature:
|
|
686
|
+
{ signature: sig, blockhash, lastValidBlockHeight },
|
|
557
687
|
commitment,
|
|
558
|
-
)
|
|
688
|
+
)
|
|
689
|
+
signatures.push(sig)
|
|
559
690
|
}
|
|
560
691
|
|
|
561
|
-
return
|
|
692
|
+
return { signatures, ephemeralAlt }
|
|
562
693
|
},
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// ============================================================================
|
|
568
|
-
// bundleForJito — standalone utility
|
|
569
|
-
// ============================================================================
|
|
570
694
|
|
|
571
|
-
/**
|
|
572
|
-
* Package TransactionSets into Jito-compatible unsigned VersionedTransactions.
|
|
573
|
-
*
|
|
574
|
-
* All transactions share the same blockhash and a Jito tip instruction is appended
|
|
575
|
-
* to the last transaction. The returned transactions are **unsigned** — the caller
|
|
576
|
-
* is responsible for signing (useful for web app flows where a wallet adapter handles signing).
|
|
577
|
-
*
|
|
578
|
-
* For a fully managed sign-and-send flow, use {@link VaultTransactionBuildResult.sendJitoBundle} instead.
|
|
579
|
-
*
|
|
580
|
-
* @param transactionSets - Ordered TransactionSets from `builder.build()`.
|
|
581
|
-
* @param params.connection - Solana RPC connection for ALT resolution and blockhash fetching.
|
|
582
|
-
* @param params.payer - Public key of the fee payer (used in the V0 message header).
|
|
583
|
-
* @param params.tipLamports - Jito tip amount in lamports, appended to the last transaction.
|
|
584
|
-
* @param params.lookupTableAddresses - All ALT addresses from the build result.
|
|
585
|
-
* @returns Unsigned VersionedTransactions ready for wallet signing.
|
|
586
|
-
*/
|
|
587
|
-
export async function bundleForJito(
|
|
588
|
-
transactionSets: TransactionSet[],
|
|
589
|
-
params: {
|
|
590
|
-
connection: Connection
|
|
591
|
-
payer: PublicKey
|
|
592
|
-
tipLamports: number
|
|
593
|
-
lookupTableAddresses: PublicKey[]
|
|
594
|
-
},
|
|
595
|
-
): Promise<VersionedTransaction[]> {
|
|
596
|
-
const { connection, payer, tipLamports, lookupTableAddresses } = params
|
|
597
|
-
|
|
598
|
-
const altAccounts = await resolveAltAccounts(connection, lookupTableAddresses)
|
|
599
|
-
const { blockhash } = await connection.getLatestBlockhash()
|
|
600
|
-
|
|
601
|
-
const transactions: VersionedTransaction[] = []
|
|
602
|
-
|
|
603
|
-
for (let i = 0; i < transactionSets.length; i++) {
|
|
604
|
-
const txSet = transactionSets[i]
|
|
605
|
-
const isLast = i === transactionSets.length - 1
|
|
606
|
-
|
|
607
|
-
const instructions = [...txSet.instructions]
|
|
608
|
-
if (isLast && tipLamports > 0) {
|
|
609
|
-
instructions.push(createJitoTipIx(tipLamports, payer))
|
|
610
695
|
}
|
|
611
|
-
|
|
612
|
-
const txAltAccounts = altAccounts.filter((alt) =>
|
|
613
|
-
txSet.addressLookupTableAddresses.some((addr) => addr.equals(alt.key)),
|
|
614
|
-
)
|
|
615
|
-
|
|
616
|
-
const messageV0 = new TransactionMessage({
|
|
617
|
-
payerKey: payer,
|
|
618
|
-
recentBlockhash: blockhash,
|
|
619
|
-
instructions,
|
|
620
|
-
}).compileToV0Message(txAltAccounts)
|
|
621
|
-
|
|
622
|
-
transactions.push(new VersionedTransaction(messageV0))
|
|
623
696
|
}
|
|
624
|
-
|
|
625
|
-
return transactions
|
|
626
697
|
}
|
|
627
698
|
|
|
628
699
|
// ============================================================================
|
|
@@ -645,6 +716,91 @@ function buildComputeBudgetIxs(
|
|
|
645
716
|
return ixs
|
|
646
717
|
}
|
|
647
718
|
|
|
719
|
+
function insertAfterLeadingComputeBudgetAndScopeInstructions(
|
|
720
|
+
instructions: TransactionInstruction[],
|
|
721
|
+
extraInstructions: TransactionInstruction[],
|
|
722
|
+
): TransactionInstruction[] {
|
|
723
|
+
if (extraInstructions.length === 0) {
|
|
724
|
+
return instructions
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
let insertIndex = 0
|
|
728
|
+
while (
|
|
729
|
+
insertIndex < instructions.length
|
|
730
|
+
&& instructions[insertIndex]?.programId.equals(ComputeBudgetProgram.programId)
|
|
731
|
+
) {
|
|
732
|
+
insertIndex += 1
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
while (
|
|
736
|
+
insertIndex < instructions.length
|
|
737
|
+
&& instructions[insertIndex]?.programId.equals(SCOPE_PROGRAM_ID)
|
|
738
|
+
) {
|
|
739
|
+
insertIndex += 1
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return [
|
|
743
|
+
...instructions.slice(0, insertIndex),
|
|
744
|
+
...extraInstructions,
|
|
745
|
+
...instructions.slice(insertIndex),
|
|
746
|
+
]
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function shouldInjectPriceRefresh(
|
|
750
|
+
txSet: TransactionSet,
|
|
751
|
+
programId: PublicKey,
|
|
752
|
+
squadsProgram: PublicKey,
|
|
753
|
+
): boolean {
|
|
754
|
+
if (!txSet.label.endsWith("-setup")) {
|
|
755
|
+
return true
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return txSet.instructions.some((instruction) =>
|
|
759
|
+
instruction.programId.equals(programId) || instruction.programId.equals(squadsProgram),
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function touchesManagedVaultPrograms(
|
|
764
|
+
instructions: TransactionInstruction[],
|
|
765
|
+
programId: PublicKey,
|
|
766
|
+
squadsProgram: PublicKey,
|
|
767
|
+
): boolean {
|
|
768
|
+
return instructions.some((instruction) =>
|
|
769
|
+
instruction.programId.equals(programId) || instruction.programId.equals(squadsProgram),
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function mergeAddressLookupTableAddresses(
|
|
774
|
+
...addressGroups: Array<readonly PublicKey[]>
|
|
775
|
+
): PublicKey[] {
|
|
776
|
+
const seen = new Set<string>()
|
|
777
|
+
const merged: PublicKey[] = []
|
|
778
|
+
|
|
779
|
+
for (const group of addressGroups) {
|
|
780
|
+
for (const address of group) {
|
|
781
|
+
const key = address.toBase58()
|
|
782
|
+
if (seen.has(key)) continue
|
|
783
|
+
seen.add(key)
|
|
784
|
+
merged.push(address)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return merged
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function createBuilderLoopscaleClient(params: {
|
|
792
|
+
connection: Connection
|
|
793
|
+
vaultPda: PublicKey
|
|
794
|
+
loopscale?: VaultTransactionBuilderLoopscaleConfig
|
|
795
|
+
}): LoopscaleClient {
|
|
796
|
+
return new LoopscaleClient({
|
|
797
|
+
connection: params.connection,
|
|
798
|
+
userWallet: params.vaultPda,
|
|
799
|
+
baseUrl: params.loopscale?.baseUrl,
|
|
800
|
+
debug: params.loopscale?.debug,
|
|
801
|
+
})
|
|
802
|
+
}
|
|
803
|
+
|
|
648
804
|
/** Fetch and resolve AddressLookupTableAccounts from on-chain, filtering nulls. */
|
|
649
805
|
async function resolveAltAccounts(
|
|
650
806
|
connection: Connection,
|
|
@@ -658,128 +814,120 @@ async function resolveAltAccounts(
|
|
|
658
814
|
.filter((v): v is AddressLookupTableAccount => v !== null)
|
|
659
815
|
}
|
|
660
816
|
|
|
661
|
-
/**
|
|
662
|
-
function
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
})
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// ── Jito bundle sending ──
|
|
672
|
-
|
|
673
|
-
type JitoRpcResponse = { result?: string; error?: unknown }
|
|
817
|
+
/** Collect all unique account keys referenced across all transaction sets. */
|
|
818
|
+
function collectUniqueAccountKeys(
|
|
819
|
+
transactionSets: TransactionSet[],
|
|
820
|
+
signer: PublicKey,
|
|
821
|
+
): PublicKey[] {
|
|
822
|
+
const seen = new Set<string>()
|
|
823
|
+
const keys: PublicKey[] = []
|
|
674
824
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
authKey?: string,
|
|
682
|
-
): Promise<JitoBundleResult> {
|
|
683
|
-
if (serializedTransactions.length > 5) {
|
|
684
|
-
throw new Error("Jito bundles support at most 5 transactions")
|
|
825
|
+
const add = (pubkey: PublicKey) => {
|
|
826
|
+
const str = pubkey.toBase58()
|
|
827
|
+
if (!seen.has(str)) {
|
|
828
|
+
seen.add(str)
|
|
829
|
+
keys.push(pubkey)
|
|
830
|
+
}
|
|
685
831
|
}
|
|
686
832
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
method: "sendBundle",
|
|
696
|
-
params: [serializedTransactions.map((tx) => tx.toString("base64")), { encoding: "base64" }],
|
|
833
|
+
add(signer)
|
|
834
|
+
for (const txSet of transactionSets) {
|
|
835
|
+
for (const ix of txSet.instructions) {
|
|
836
|
+
add(ix.programId)
|
|
837
|
+
for (const key of ix.keys) {
|
|
838
|
+
add(key.pubkey)
|
|
839
|
+
}
|
|
840
|
+
}
|
|
697
841
|
}
|
|
698
842
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
// Fan out to regional endpoints (best-effort, don't await)
|
|
703
|
-
const regionalPromises = JITO_REGIONAL_URLS.map((url) =>
|
|
704
|
-
fetch(`${url}${JITO_BUNDLES_PATH}`, { method: "POST", headers, body: JSON.stringify(requestBody) })
|
|
705
|
-
.catch(() => {}),
|
|
706
|
-
)
|
|
707
|
-
Promise.allSettled(regionalPromises)
|
|
843
|
+
return keys
|
|
844
|
+
}
|
|
708
845
|
|
|
709
|
-
|
|
710
|
-
|
|
846
|
+
/** Validate that every transaction set can serialize within the 1232-byte limit. */
|
|
847
|
+
function validateTransactionSizes(
|
|
848
|
+
transactionSets: TransactionSet[],
|
|
849
|
+
altAccounts: AddressLookupTableAccount[],
|
|
850
|
+
signer: PublicKey,
|
|
851
|
+
extraAlt?: AddressLookupTableAccount,
|
|
852
|
+
): void {
|
|
853
|
+
for (const txSet of transactionSets) {
|
|
854
|
+
const txAltAccounts = altAccounts.filter((alt) =>
|
|
855
|
+
txSet.addressLookupTableAddresses.some((addr) => addr.equals(alt.key)),
|
|
856
|
+
)
|
|
857
|
+
if (extraAlt && !txAltAccounts.some((a) => a.key.equals(extraAlt.key))) {
|
|
858
|
+
txAltAccounts.push(extraAlt)
|
|
859
|
+
}
|
|
711
860
|
try {
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
})
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
861
|
+
const testMsg = new TransactionMessage({
|
|
862
|
+
payerKey: signer,
|
|
863
|
+
recentBlockhash: PublicKey.default.toBase58(),
|
|
864
|
+
instructions: txSet.instructions,
|
|
865
|
+
}).compileToV0Message(txAltAccounts)
|
|
866
|
+
const testTx = new VersionedTransaction(testMsg)
|
|
867
|
+
const serialized = testTx.serialize()
|
|
868
|
+
if (serialized.length > 1232) {
|
|
869
|
+
throw new Error(
|
|
870
|
+
`Transaction "${txSet.label}" is oversized (${serialized.length} bytes, limit 1232). ` +
|
|
871
|
+
`This usually means too many actions were packed into a single addActions() call. ` +
|
|
872
|
+
`Split them into separate addActions() calls so each gets its own sync transaction.`,
|
|
873
|
+
)
|
|
722
874
|
}
|
|
723
|
-
|
|
724
|
-
if (
|
|
725
|
-
|
|
875
|
+
} catch (e) {
|
|
876
|
+
if (e instanceof Error) {
|
|
877
|
+
const ixCount = txSet.instructions.length
|
|
878
|
+
const altCount = txAltAccounts.length
|
|
879
|
+
const accountCount = txSet.instructions.reduce((sum, ix) => sum + ix.keys.length, 0)
|
|
880
|
+
throw new Error(
|
|
881
|
+
`Transaction "${txSet.label}" failed to serialize: ${e.message}. ` +
|
|
882
|
+
`Transaction has ${ixCount} instructions, ${accountCount} account keys, ` +
|
|
883
|
+
`and ${altCount} address lookup table(s). ` +
|
|
884
|
+
`Try splitting actions into separate addActions() calls.`,
|
|
885
|
+
)
|
|
726
886
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
await new Promise((r) => setTimeout(r, JITO_RETRY_DELAY_MS))
|
|
887
|
+
throw e
|
|
888
|
+
}
|
|
730
889
|
}
|
|
731
|
-
|
|
732
|
-
return { bundleId: "", signatures }
|
|
733
890
|
}
|
|
734
891
|
|
|
735
|
-
|
|
892
|
+
/** Split an array into chunks of the given size. */
|
|
893
|
+
function chunkArray<T>(arr: T[], size: number): T[][] {
|
|
894
|
+
const chunks: T[][] = []
|
|
895
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
896
|
+
chunks.push(arr.slice(i, i + size))
|
|
897
|
+
}
|
|
898
|
+
return chunks
|
|
899
|
+
}
|
|
736
900
|
|
|
737
|
-
/**
|
|
738
|
-
|
|
739
|
-
* Batches in groups of 30 addresses (Solana limit per extend instruction).
|
|
740
|
-
* Waits 2 seconds after extension for ALT propagation.
|
|
741
|
-
*
|
|
742
|
-
* @internal Not currently wired into any flow. Reserved for when ALT management
|
|
743
|
-
* needs to be handled by the builder (e.g., oversized Loopscale transactions).
|
|
744
|
-
*/
|
|
745
|
-
async function autoExtendAlt(
|
|
901
|
+
/** Build, sign, send, and confirm a simple transaction. */
|
|
902
|
+
async function sendAndConfirmTransaction(
|
|
746
903
|
connection: Connection,
|
|
747
|
-
lookupTable: PublicKey,
|
|
748
|
-
authority: web3.Signer,
|
|
749
904
|
instructions: TransactionInstruction[],
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const extendIx = AddressLookupTableProgram.extendLookupTable({
|
|
766
|
-
payer: authority.publicKey,
|
|
767
|
-
authority: authority.publicKey,
|
|
768
|
-
lookupTable,
|
|
769
|
-
addresses: batch,
|
|
770
|
-
})
|
|
771
|
-
|
|
772
|
-
const { blockhash } = await connection.getLatestBlockhash()
|
|
773
|
-
const msg = new TransactionMessage({
|
|
774
|
-
payerKey: authority.publicKey,
|
|
775
|
-
recentBlockhash: blockhash,
|
|
776
|
-
instructions: [extendIx],
|
|
777
|
-
}).compileToV0Message()
|
|
905
|
+
signers: web3.Signer[],
|
|
906
|
+
commitment: web3.Commitment,
|
|
907
|
+
): Promise<string> {
|
|
908
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(commitment)
|
|
909
|
+
const msg = new TransactionMessage({
|
|
910
|
+
payerKey: signers[0]!.publicKey,
|
|
911
|
+
recentBlockhash: blockhash,
|
|
912
|
+
instructions,
|
|
913
|
+
}).compileToV0Message()
|
|
914
|
+
const tx = new VersionedTransaction(msg)
|
|
915
|
+
tx.sign(signers)
|
|
916
|
+
const sig = await connection.sendTransaction(tx)
|
|
917
|
+
await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, commitment)
|
|
918
|
+
return sig
|
|
919
|
+
}
|
|
778
920
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
921
|
+
/** Poll until the current slot advances past the target slot. */
|
|
922
|
+
async function waitForSlotAdvance(
|
|
923
|
+
connection: Connection,
|
|
924
|
+
targetSlot: number,
|
|
925
|
+
commitment: web3.Commitment = "processed",
|
|
926
|
+
): Promise<void> {
|
|
927
|
+
for (let i = 0; i < 60; i++) {
|
|
928
|
+
const currentSlot = await connection.getSlot(commitment)
|
|
929
|
+
if (currentSlot > targetSlot) return
|
|
930
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
782
931
|
}
|
|
783
|
-
|
|
784
|
-
await new Promise((r) => setTimeout(r, 2000))
|
|
932
|
+
throw new Error("Timed out waiting for slot to advance after ephemeral ALT creation")
|
|
785
933
|
}
|