@cowprotocol/sdk-trading-solana 0.6.0 → 0.8.0

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/README.md CHANGED
@@ -10,51 +10,232 @@
10
10
  | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
11
11
  | ![Statements](https://img.shields.io/badge/statements-85.64%25-yellow.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-94.44%25-brightgreen.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-58.13%25-red.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-93.78%25-brightgreen.svg?style=flat) |
12
12
 
13
- CoW Protocol's Solana settlement support: Jupiter-sourced quotes and on-chain `CreateOrder`
14
- posting against the CoW Protocol Solana settlement program.
13
+ `@cowprotocol/sdk-trading-solana` is CoW Protocol's Solana settlement client: it turns a quote
14
+ into an on-chain `CreateOrder` instruction against the CoW Protocol Solana
15
+ settlement program.
15
16
 
16
- **Experimental.** The Solana settlement program this package targets isn't deployed anywhere
17
- reachable yet. See `docs/superpowers/specs/2026-09-01-sdk-trading-solana-extraction-design.md` in
18
- the `cowswap` repo for background.
17
+ **Experimental.** The settlement program (`cow-settlement-interface` / `solana-programs`) is a young,
18
+ actively evolving deployment expect breaking changes to the intent wire format, PDA seeds and this
19
+ package's API across minor versions. The currently deployed program id is exported as
20
+ [`SOLANA_SETTLEMENT_PROGRAM_ID`](https://github.com/cowprotocol/cow-sdk/blob/main/packages/config/src/chains/const/contracts.ts)
21
+ from `@cowprotocol/sdk-config` — see it on
22
+ [Solana Explorer](https://explorer.solana.com/address/FYp8R5K4B3B1Kfr7QuWzMz4TwoT7wptjYtxgCrY5sRXb).
19
23
 
20
- ## Usage
24
+ ```
25
+ npm install @cowprotocol/sdk-trading-solana
26
+ ```
27
+
28
+ A runnable end-to-end example lives in [`examples/nodejs/solana`](../../examples/nodejs/solana).
29
+
30
+ ## Making a trade
31
+
32
+ ### Step 0 — approve the settlement program as SPL delegate
33
+
34
+ The settlement program moves funds out of the seller's token account via a CPI, which SPL Token
35
+ only allows for an approved `delegate`. Before any order on a given sell-token account can settle,
36
+ that account's owner must approve the settlement **state PDA** (not the program id itself) as
37
+ delegate, for at least the amount being sold. `approveCowProtocol` builds that instruction without
38
+ sending it, deriving the sell-token account and delegate for you:
39
+
40
+ ```ts
41
+ const sdk = new SolanaTradingSdk() // same env as the SDK instance used for the quote below
42
+
43
+ const approveInstruction = sdk.approveCowProtocol({
44
+ ownerAddress: owner,
45
+ sellTokenAddress: sellMint,
46
+ approveAmount, // at least the order's sellAmount
47
+ })
48
+ ```
21
49
 
22
- Quoting needs no signer:
50
+ This approval is **not one-time**: like any SPL delegate, each settled order decrements the
51
+ delegate's remaining allowance by the amount actually transferred, so approving once for exactly one
52
+ order's `sellAmount` leaves nothing for the next one. Check the sell-token account's current
53
+ `delegate`/`delegatedAmount` before building an order, and reapprove (topping the allowance back up)
54
+ whenever it's insufficient — approving a new amount replaces the previous allowance rather than
55
+ adding to it. Separately, because the delegate PDA is derived from the settlement program's
56
+ major/minor version (`getSettlementSeed`), a program upgrade that bumps that version changes the
57
+ delegate address entirely, requiring a fresh approval regardless of remaining allowance. This step
58
+ can be sent as its own transaction ahead of time, or bundled with the `CreateOrder` instruction in
59
+ the same transaction — see [Step 2](#step-2--create-the-order) below.
60
+
61
+ ### Step 1 — get a quote
62
+
63
+ Quoting needs no signer, just the addresses and amount:
23
64
 
24
65
  ```ts
66
+ import { OrderKind } from '@cowprotocol/sdk-order-book'
25
67
  import { SolanaTradingSdk } from '@cowprotocol/sdk-trading-solana'
26
68
 
27
- const sdk = new SolanaTradingSdk()
69
+ const sdk = new SolanaTradingSdk() // pass `{ env: 'staging' }` to target staging
70
+
28
71
  const { quoteResults, solanaQuote, buildOrder, postSwapOrderFromQuote } = await sdk.getQuote({
29
- ownerAddress,
30
- receiverAddress,
72
+ ownerAddress, // the seller's wallet — signs the CreateOrder transaction
31
73
  sellTokenAddress,
32
74
  sellTokenDecimals,
33
75
  buyTokenAddress,
34
76
  buyTokenDecimals,
35
- amount,
36
- kind,
77
+ amount, // bigint — sell-side amount for a SELL order, buy-side amount for a BUY order
78
+ kind: OrderKind.SELL,
37
79
  })
38
80
  ```
39
81
 
40
- ### Let the SDK submit the order
82
+ - `quoteResults` mirrors the EVM SDK's [`QuoteResults`](../trading/src/types.ts) (trade parameters, suggested slippage, amounts
83
+ and costs) so UI code can share formatting logic across chains.
84
+ - `solanaQuote` is the Solana-specific quote: the order **intent** that was built from the quote
85
+ response, its encoded bytes, its `uid`, the order PDA it will live at, and the raw upstream quote
86
+ (real amounts/slippage) the intent was derived from.
87
+ - Native SOL is accepted as the sentinel address; it's substituted with wrapped SOL (WSOL) before
88
+ quoting and building the intent, since Solana has no native-token mint to reference on-chain.
89
+ - Passing `sellTokenProgramId` / `buyTokenProgramId` (e.g. `TOKEN_2022_PROGRAM_ID`) is required for
90
+ Token-2022 mints, since the associated token account address differs by program.
41
91
 
42
- `signAndSend` is passed at the point of signing:
92
+ ### Step 2 create the order
93
+
94
+ Two ways to get the order on-chain, depending on whether you need to bundle it with other
95
+ instructions:
96
+
97
+ **Let the SDK submit it as its own transaction** — pass a signing/sending function at the point of
98
+ signing:
43
99
 
44
100
  ```ts
45
101
  const result = await postSwapOrderFromQuote(signAndSend)
102
+ // result.orderId, result.txHash / result.signature
103
+ ```
104
+
105
+ `signAndSend` has the shape `(instruction: TransactionInstruction) => Promise<{ signature: string }>`
106
+ — build, sign and send the transaction however fits your app (a `Connection` + `Keypair`, a wallet
107
+ adapter, a Squads/Safe-style multisig flow, etc).
108
+
109
+ **Bundle the order with your own instructions** — a Solana order is created by a single instruction,
110
+ so it can share one transaction with, say, the Step 0 delegate approval, a native-SOL wrap, or
111
+ anything else. `buildOrder` returns that instruction without sending it:
112
+
113
+ ```ts
114
+ const { instruction, orderId, orderPda, uid } = await buildOrder()
115
+
116
+ await sendMyTransaction([approveInstruction, instruction])
46
117
  ```
47
118
 
48
- ### Bundle the order with your own instructions
119
+ This is the recommended path whenever the sell-token account's delegate allowance needs
120
+ topping up for this trade, since it lets the delegate approval and the order creation land
121
+ atomically in one transaction.
49
122
 
50
- A Solana order is created by a single instruction, so it can share a transaction with, say, a wrap and a
51
- token delegation. `buildOrder` returns that instruction without sending it:
123
+ Both paths accept the same optional `advancedSettings` (currently `quoteRequest.receiver` and
124
+ `quoteRequest.validTo`) and apply them identically overriding `receiver` or `validTo` re-derives
125
+ the order's `uid` and PDA so they still match the intent actually being created:
52
126
 
53
127
  ```ts
54
- const { instruction, orderId } = await buildOrder()
128
+ await postSwapOrderFromQuote(signAndSend, { quoteRequest: { receiver: otherWallet.toBase58() } })
129
+ ```
130
+
131
+ ### Step 3 — read back the result
132
+
133
+ Both `postSwapOrderFromQuote` and `buildOrder` return an `orderId` — the `uid` as the `0x`-prefixed
134
+ hex string the CoW order-book API uses for EVM orders too, so it can be handled uniformly by shared
135
+ UI/state code. `solanaQuote.orderPda` is the on-chain account the order lives at once the
136
+ `CreateOrder` instruction lands.
55
137
 
56
- await sendMyTransaction([...wrapInstructions, approveInstruction, instruction])
138
+ ## The intent model
139
+
140
+ Every Solana order is, at its core, a `SolanaOrderIntent` — a fixed-layout struct that is the direct
141
+ TypeScript counterpart of `OrderIntent` in `cow-settlement-interface` (`interface/src/data/intent.rs`).
142
+ Unlike an EVM order (an EIP-712 message the owner signs off-chain and the order-book stores), a
143
+ Solana intent is never signed as data — it's written directly into an on-chain account by an
144
+ instruction the owner's wallet signs.
145
+
146
+ ```ts
147
+ interface SolanaOrderIntent {
148
+ owner: PublicKey // signs the CreateOrder transaction
149
+ buyTokenAccount: PublicKey // receives the buy-side proceeds — implicitly encodes the receiver
150
+ buyMint: PublicKey
151
+ sellTokenAccount: PublicKey // funds pulled from here — must be owned by `owner`; implicitly encodes the spender
152
+ sellMint: PublicKey
153
+ sellAmount: bigint
154
+ buyAmount: bigint
155
+ validTo: number // unix timestamp, seconds
156
+ kind: OrderKind // SELL | BUY
157
+ partiallyFillable: boolean
158
+ createdOnChain: boolean // must be true — see "Authentication" below
159
+ appData: Uint8Array // exactly 32 bytes, opaque to the settlement program; no convention defined yet, sent as zeroes
160
+ }
57
161
  ```
58
162
 
59
- Both paths apply `advancedSettings` identically — overriding `receiver` or `validTo` re-derives the order's
60
- `uid` and PDA so they still match the intent actually being created.
163
+ ### Encoding
164
+
165
+ `encodeOrderIntent` packs the intent into the exact 213-byte (`ENCODED_ORDER_INTENT_SIZE`) little-endian
166
+ layout the settlement program reads (`EncodedOrderIntent::from(&OrderIntent)`):
167
+
168
+ | Bytes | Field |
169
+ | ----- | --------------------------------- |
170
+ | 32 | `owner` |
171
+ | 32 | `buyTokenAccount` |
172
+ | 32 | `buyMint` |
173
+ | 32 | `sellTokenAccount` |
174
+ | 32 | `sellMint` |
175
+ | 8 | `sellAmount` (u64 LE) |
176
+ | 8 | `buyAmount` (u64 LE) |
177
+ | 4 | `validTo` (u32 LE) |
178
+ | 1 | flags (bit 0 `createdOnChain`, bit 1 `kind === BUY`, bit 2 `partiallyFillable`) |
179
+ | 32 | `appData` |
180
+
181
+ ### Identity: uid and the order PDA
182
+
183
+ `hashOrderIntent` takes the SHA-256 digest of the encoded intent bytes. That 32-byte digest is both:
184
+
185
+ - the order's **uid** (`toOrderId` formats it as the `0x`-prefixed id used across the SDK), and
186
+ - the middle seed of the order's **program-derived address** (`findOrderPda`), alongside a
187
+ version-embedded settlement seed (`getSettlementSeed`, `"settlement v" + version`, padded to a
188
+ fixed width so seeds from different versions can't collide) and a trailing `"order"` seed.
189
+
190
+ Because the uid is a hash of the intent's content, any change to the intent — including an
191
+ `advancedSettings` override to `receiver` or `validTo` — changes the uid and therefore the PDA the
192
+ order will be created at. `buildSolanaSwapOrder` re-derives both whenever `advancedSettings`
193
+ actually changes the intent, so the instruction it returns always targets the PDA matching the
194
+ intent it encodes.
195
+
196
+ ### The `CreateOrder` instruction
197
+
198
+ `buildCreateOrderInstruction` builds the instruction that writes the intent on-chain, matching
199
+ `CreateOrder::into::<Instruction>()`:
200
+
201
+ - **data**: `[discriminator = 2, ...213 intent bytes]`
202
+ - **accounts**: `owner` (readonly signer), `createdBy` (writable signer — funds the new order PDA's
203
+ rent; may equal `owner`), `orderPda` (writable), the System Program
204
+
205
+ ### Authentication: why `createdOnChain` must be `true`
206
+
207
+ `cow-settlement-interface` supports two ways an intent can be authenticated: created on-chain (the
208
+ owner signs the `CreateOrder` transaction themselves) or via an off-chain Ed25519-presigned order
209
+ that anyone can submit on the owner's behalf. This SDK only implements the former —
210
+ `encodeOrderIntent` always sets the on-chain flag, and the settlement program authenticates the
211
+ order against the transaction's own signature rather than a separate signed payload. This is the
212
+ Solana counterpart of an EVM `PRESIGN` order (`buildSolanaSwapOrder` reports
213
+ `signingScheme: SigningScheme.PRESIGN` for exactly this reason) rather than an EIP-712 `EIP712` one.
214
+
215
+ ## Example app
216
+
217
+ [`examples/nodejs/solana`](../../examples/nodejs/solana) is a runnable, end-to-end demo: it gets a
218
+ quote, approves the settlement program's delegate, builds the `CreateOrder` instruction, bundles
219
+ both into one transaction and submits it. **It targets mainnet-beta by default and trades real
220
+ funds** — see that example's own README for setup, including how to point it at devnet instead.
221
+
222
+ ## Why Solana trading looks different from EVM trading
223
+
224
+ On EVM chains, `TradingSdk` gets a quote from the CoW order-book API, has the caller sign an
225
+ EIP-712 typed order, and POSTs the signed order to the order-book, which later settles it. Solana
226
+ has neither an order-book API to post to nor an implicit global signer:
227
+
228
+ - **Quotes don't come from the CoW order-book (yet).** `SolanaTradingSdk.getQuote` currently sources
229
+ pricing and route discovery from an external quoting API, then builds a CoW Protocol order
230
+ **intent** from the result. That API is used for quoting only — orders are never submitted through
231
+ it, only ever created via the CoW settlement program. This is expected to move onto the CoW
232
+ order-book API directly as Solana support matures.
233
+ - **Orders are created entirely on-chain.** There is no signed order body to POST. Creating an order
234
+ means submitting a single `CreateOrder` instruction that writes the order intent into a new
235
+ program-derived account (the order PDA). The transaction signature that lands on-chain *is* the
236
+ order's authentication.
237
+ - **No bound signer.** The EVM SDK gets its signer from a global adapter set once at app startup.
238
+ This package has no such adapter: quoting needs no signer at all, and a signing/sending function
239
+ (`SolanaSignAndSend`) is passed only at the point of actually submitting a transaction. That also
240
+ means a quote can be turned into a raw `TransactionInstruction` (via `buildOrder`) without ever
241
+ involving a signer, so it can be composed into a larger transaction built and sent by the caller.
package/dist/index.d.mts CHANGED
@@ -29,16 +29,16 @@ declare class JupiterAPI {
29
29
  }
30
30
 
31
31
  /**
32
- * TS port of `cow-settlement-interface`'s `OrderIntent` (interface/src/data/intent.rs, v0.3.0).
32
+ * TS port of `cow-settlement-interface`'s `OrderIntent` (interface/src/data/intent.rs, v0.4.0).
33
33
  * Every field here has a Rust counterpart with the same name; keep them in sync if the settlement
34
34
  * program's wire format changes.
35
35
  */
36
36
  interface SolanaOrderIntent {
37
37
  owner: PublicKey;
38
- buyTokenAccount: PublicKey;
39
- buyMint: PublicKey;
40
38
  sellTokenAccount: PublicKey;
41
39
  sellMint: PublicKey;
40
+ buyTokenAccount: PublicKey;
41
+ buyMint: PublicKey;
42
42
  sellAmount: bigint;
43
43
  buyAmount: bigint;
44
44
  /** Unix timestamp seconds. */
@@ -73,7 +73,6 @@ declare function toOrderId(uid: Uint8Array): string;
73
73
 
74
74
  interface SolanaQuoteParameters {
75
75
  ownerAddress: PublicKeyInitData;
76
- receiverAddress: PublicKeyInitData;
77
76
  sellTokenAddress: PublicKeyInitData;
78
77
  sellTokenDecimals: number;
79
78
  buyTokenAddress: PublicKeyInitData;
@@ -81,6 +80,7 @@ interface SolanaQuoteParameters {
81
80
  /** Sell-side amount for a SELL order, buy-side amount for a BUY order — same convention as Jupiter's `amount`. */
82
81
  amount: bigint;
83
82
  kind: OrderKind;
83
+ receiverAddress?: PublicKeyInitData;
84
84
  partiallyFillable?: boolean;
85
85
  /** Order lifetime from now, in seconds. Defaults to 30 minutes. */
86
86
  validForSeconds?: number;
@@ -217,6 +217,15 @@ declare function postSolanaSwapOrderFromQuote(quote: SolanaSwapOrderQuote, signA
217
217
  interface SolanaTradingSdkOptions {
218
218
  env?: CowEnv;
219
219
  }
220
+ interface ApproveCowProtocolParams {
221
+ ownerAddress: PublicKeyInitData;
222
+ sellTokenAddress: PublicKeyInitData;
223
+ /** Amount to approve, at least the order's `sellAmount`. */
224
+ approveAmount: bigint;
225
+ /** Token program owning the sell mint's accounts (classic SPL Token vs Token-2022). Defaults to the
226
+ * classic SPL Token program — pass `TOKEN_2022_PROGRAM_ID` explicitly for Token-2022 mints. */
227
+ sellTokenProgramId?: PublicKeyInitData;
228
+ }
220
229
  /**
221
230
  * Solana counterpart to `QuoteAndPost`. It additionally exposes `solanaQuote` and `buildOrder`, because a
222
231
  * Solana order is a plain instruction: callers may want to bundle it with a wrap or a token delegation and
@@ -239,7 +248,14 @@ interface SolanaQuoteAndPost {
239
248
  declare class SolanaTradingSdk {
240
249
  private readonly options;
241
250
  constructor(options?: SolanaTradingSdkOptions);
251
+ /**
252
+ * Builds the SPL `approve` instruction that delegates `sellAmount` of `sellTokenAddress` to the
253
+ * settlement program, without sending it — see "Step 0" in the README for why this is required
254
+ * before an order on a given sell-token account can settle. Bundle it with `buildOrder`'s
255
+ * instruction in the same transaction the first time a wallet trades a given token.
256
+ */
257
+ approveCowProtocol(params: ApproveCowProtocolParams): TransactionInstruction;
242
258
  getQuote(params: SolanaQuoteParameters): Promise<SolanaQuoteAndPost>;
243
259
  }
244
260
 
245
- export { type CreateOrderInstructionParams, ENCODED_ORDER_INTENT_SIZE, JupiterAPI, type JupiterOrderRequest, type JupiterOrderResponse, ORDER_SEED, type SolanaOrderIntent, type SolanaQuote, type SolanaQuoteAndPost, type SolanaQuoteParameters, type SolanaSignAndSend, type SolanaSwapOrder, type SolanaSwapOrderQuote, SolanaTradingSdk, type SolanaTradingSdkOptions, buildCreateOrderInstruction, buildSolanaSwapOrder, encodeOrderIntent, findOrderPda, findSettlementStatePda, getSettlementSeed, getSolanaDelegateAuthority, getSolanaQuote, getSolanaSettlementProgramId, hashOrderIntent, postSolanaSwapOrderFromQuote, toHex, toOrderId };
261
+ export { type ApproveCowProtocolParams, type CreateOrderInstructionParams, ENCODED_ORDER_INTENT_SIZE, JupiterAPI, type JupiterOrderRequest, type JupiterOrderResponse, ORDER_SEED, type SolanaOrderIntent, type SolanaQuote, type SolanaQuoteAndPost, type SolanaQuoteParameters, type SolanaSignAndSend, type SolanaSwapOrder, type SolanaSwapOrderQuote, SolanaTradingSdk, type SolanaTradingSdkOptions, buildCreateOrderInstruction, buildSolanaSwapOrder, encodeOrderIntent, findOrderPda, findSettlementStatePda, getSettlementSeed, getSolanaDelegateAuthority, getSolanaQuote, getSolanaSettlementProgramId, hashOrderIntent, postSolanaSwapOrderFromQuote, toHex, toOrderId };
package/dist/index.d.ts CHANGED
@@ -29,16 +29,16 @@ declare class JupiterAPI {
29
29
  }
30
30
 
31
31
  /**
32
- * TS port of `cow-settlement-interface`'s `OrderIntent` (interface/src/data/intent.rs, v0.3.0).
32
+ * TS port of `cow-settlement-interface`'s `OrderIntent` (interface/src/data/intent.rs, v0.4.0).
33
33
  * Every field here has a Rust counterpart with the same name; keep them in sync if the settlement
34
34
  * program's wire format changes.
35
35
  */
36
36
  interface SolanaOrderIntent {
37
37
  owner: PublicKey;
38
- buyTokenAccount: PublicKey;
39
- buyMint: PublicKey;
40
38
  sellTokenAccount: PublicKey;
41
39
  sellMint: PublicKey;
40
+ buyTokenAccount: PublicKey;
41
+ buyMint: PublicKey;
42
42
  sellAmount: bigint;
43
43
  buyAmount: bigint;
44
44
  /** Unix timestamp seconds. */
@@ -73,7 +73,6 @@ declare function toOrderId(uid: Uint8Array): string;
73
73
 
74
74
  interface SolanaQuoteParameters {
75
75
  ownerAddress: PublicKeyInitData;
76
- receiverAddress: PublicKeyInitData;
77
76
  sellTokenAddress: PublicKeyInitData;
78
77
  sellTokenDecimals: number;
79
78
  buyTokenAddress: PublicKeyInitData;
@@ -81,6 +80,7 @@ interface SolanaQuoteParameters {
81
80
  /** Sell-side amount for a SELL order, buy-side amount for a BUY order — same convention as Jupiter's `amount`. */
82
81
  amount: bigint;
83
82
  kind: OrderKind;
83
+ receiverAddress?: PublicKeyInitData;
84
84
  partiallyFillable?: boolean;
85
85
  /** Order lifetime from now, in seconds. Defaults to 30 minutes. */
86
86
  validForSeconds?: number;
@@ -217,6 +217,15 @@ declare function postSolanaSwapOrderFromQuote(quote: SolanaSwapOrderQuote, signA
217
217
  interface SolanaTradingSdkOptions {
218
218
  env?: CowEnv;
219
219
  }
220
+ interface ApproveCowProtocolParams {
221
+ ownerAddress: PublicKeyInitData;
222
+ sellTokenAddress: PublicKeyInitData;
223
+ /** Amount to approve, at least the order's `sellAmount`. */
224
+ approveAmount: bigint;
225
+ /** Token program owning the sell mint's accounts (classic SPL Token vs Token-2022). Defaults to the
226
+ * classic SPL Token program — pass `TOKEN_2022_PROGRAM_ID` explicitly for Token-2022 mints. */
227
+ sellTokenProgramId?: PublicKeyInitData;
228
+ }
220
229
  /**
221
230
  * Solana counterpart to `QuoteAndPost`. It additionally exposes `solanaQuote` and `buildOrder`, because a
222
231
  * Solana order is a plain instruction: callers may want to bundle it with a wrap or a token delegation and
@@ -239,7 +248,14 @@ interface SolanaQuoteAndPost {
239
248
  declare class SolanaTradingSdk {
240
249
  private readonly options;
241
250
  constructor(options?: SolanaTradingSdkOptions);
251
+ /**
252
+ * Builds the SPL `approve` instruction that delegates `sellAmount` of `sellTokenAddress` to the
253
+ * settlement program, without sending it — see "Step 0" in the README for why this is required
254
+ * before an order on a given sell-token account can settle. Bundle it with `buildOrder`'s
255
+ * instruction in the same transaction the first time a wallet trades a given token.
256
+ */
257
+ approveCowProtocol(params: ApproveCowProtocolParams): TransactionInstruction;
242
258
  getQuote(params: SolanaQuoteParameters): Promise<SolanaQuoteAndPost>;
243
259
  }
244
260
 
245
- export { type CreateOrderInstructionParams, ENCODED_ORDER_INTENT_SIZE, JupiterAPI, type JupiterOrderRequest, type JupiterOrderResponse, ORDER_SEED, type SolanaOrderIntent, type SolanaQuote, type SolanaQuoteAndPost, type SolanaQuoteParameters, type SolanaSignAndSend, type SolanaSwapOrder, type SolanaSwapOrderQuote, SolanaTradingSdk, type SolanaTradingSdkOptions, buildCreateOrderInstruction, buildSolanaSwapOrder, encodeOrderIntent, findOrderPda, findSettlementStatePda, getSettlementSeed, getSolanaDelegateAuthority, getSolanaQuote, getSolanaSettlementProgramId, hashOrderIntent, postSolanaSwapOrderFromQuote, toHex, toOrderId };
261
+ export { type ApproveCowProtocolParams, type CreateOrderInstructionParams, ENCODED_ORDER_INTENT_SIZE, JupiterAPI, type JupiterOrderRequest, type JupiterOrderResponse, ORDER_SEED, type SolanaOrderIntent, type SolanaQuote, type SolanaQuoteAndPost, type SolanaQuoteParameters, type SolanaSignAndSend, type SolanaSwapOrder, type SolanaSwapOrderQuote, SolanaTradingSdk, type SolanaTradingSdkOptions, buildCreateOrderInstruction, buildSolanaSwapOrder, encodeOrderIntent, findOrderPda, findSettlementStatePda, getSettlementSeed, getSolanaDelegateAuthority, getSolanaQuote, getSolanaSettlementProgramId, hashOrderIntent, postSolanaSwapOrderFromQuote, toHex, toOrderId };
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -66,10 +76,10 @@ function encodeOrderIntent(intent) {
66
76
  offset += 8;
67
77
  };
68
78
  writePubkey(intent.owner);
69
- writePubkey(intent.buyTokenAccount);
70
- writePubkey(intent.buyMint);
71
79
  writePubkey(intent.sellTokenAccount);
72
80
  writePubkey(intent.sellMint);
81
+ writePubkey(intent.buyTokenAccount);
82
+ writePubkey(intent.buyMint);
73
83
  writeU64LE(intent.sellAmount);
74
84
  writeU64LE(intent.buyAmount);
75
85
  view.setUint32(offset, intent.validTo, true);
@@ -228,7 +238,7 @@ async function getSolanaQuote(params, options = {}) {
228
238
  const {
229
239
  slippageBps: slippageBpsOverride,
230
240
  ownerAddress,
231
- receiverAddress,
241
+ receiverAddress = ownerAddress,
232
242
  sellTokenDecimals,
233
243
  buyTokenDecimals,
234
244
  amount,
@@ -259,7 +269,7 @@ async function getSolanaQuote(params, options = {}) {
259
269
  amount: amount.toString(),
260
270
  swapMode: kind === import_sdk_order_book2.OrderKind.SELL ? "ExactIn" : "ExactOut"
261
271
  });
262
- const suggestedSlippageBps = slippageBpsOverride ?? jupiterOrder.slippageBps;
272
+ const signedSlippageBps = slippageBpsOverride ?? jupiterOrder.slippageBps;
263
273
  const validTo = Math.floor(Date.now() / 1e3) + validForSeconds;
264
274
  const orderParams = {
265
275
  sellToken: sellTokenAddress,
@@ -280,7 +290,7 @@ async function getSolanaQuote(params, options = {}) {
280
290
  };
281
291
  const amountsAndCosts = (0, import_sdk_order_book2.getQuoteAmountsAndCosts)({
282
292
  orderParams,
283
- slippagePercentBps: suggestedSlippageBps,
293
+ slippagePercentBps: signedSlippageBps,
284
294
  // TODO: implement fees
285
295
  partnerFeeBps: 0,
286
296
  protocolFeeBps: 0
@@ -337,7 +347,9 @@ async function getSolanaQuote(params, options = {}) {
337
347
  const quoteResults = {
338
348
  quoteResponse,
339
349
  amountsAndCosts,
340
- suggestedSlippageBps,
350
+ // What the quote provider suggested, never the caller's own `slippageBps`: consumers read this as a
351
+ // recommendation and would otherwise be handed their own input back as advice.
352
+ suggestedSlippageBps: jupiterOrder.slippageBps,
341
353
  tradeParameters,
342
354
  orderToSign: {},
343
355
  appDataInfo: {},
@@ -350,27 +362,49 @@ async function getSolanaQuote(params, options = {}) {
350
362
  var import_sdk_order_book3 = require("@cowprotocol/sdk-order-book");
351
363
  var import_spl_token2 = require("@solana/spl-token");
352
364
  var import_web36 = require("@solana/web3.js");
365
+
366
+ // src/appData.ts
367
+ var import_sdk_app_data = require("@cowprotocol/sdk-app-data");
368
+ var import_deepmerge = __toESM(require("deepmerge"));
369
+ async function mergeAppData(doc, override) {
370
+ const clearedDoc = {
371
+ ...doc,
372
+ metadata: {
373
+ ...doc.metadata,
374
+ ...override.metadata?.hooks ? { hooks: {} } : {},
375
+ ...override.metadata?.userConsents ? { userConsents: [] } : {}
376
+ }
377
+ };
378
+ const merged = (0, import_deepmerge.default)(clearedDoc, override);
379
+ const fullAppData = await (0, import_sdk_app_data.stringifyDeterministic)(merged);
380
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(fullAppData));
381
+ return new Uint8Array(digest);
382
+ }
383
+
384
+ // src/buildSwapOrder.ts
353
385
  async function buildSolanaSwapOrder({ quoteResults, solanaQuote }, advancedSettings) {
354
- const intent = { ...solanaQuote.intent };
355
- let uid = solanaQuote.uid;
356
- let orderPda = solanaQuote.orderPda;
357
- if (advancedSettings?.quoteRequest) {
358
- const { validTo, receiver } = advancedSettings.quoteRequest;
359
- if (receiver) {
360
- intent.buyTokenAccount = (0, import_spl_token2.getAssociatedTokenAddressSync)(
361
- intent.buyMint,
386
+ const { validTo, receiver } = advancedSettings?.quoteRequest ?? {};
387
+ const overrides = {
388
+ ...receiver && {
389
+ buyTokenAccount: (0, import_spl_token2.getAssociatedTokenAddressSync)(
390
+ solanaQuote.intent.buyMint,
362
391
  new import_web36.PublicKey(receiver),
363
392
  false,
364
393
  solanaQuote.buyTokenProgramId
365
- );
366
- }
367
- if (validTo)
368
- intent.validTo = validTo;
369
- if (receiver || validTo) {
370
- const intentBytes = encodeOrderIntent(intent);
371
- uid = await hashOrderIntent(intentBytes);
372
- [orderPda] = findOrderPda(solanaQuote.programId, uid);
394
+ )
395
+ },
396
+ ...validTo && { validTo },
397
+ ...advancedSettings?.appData && {
398
+ appData: await mergeAppData(quoteResults.appDataInfo.doc ?? {}, advancedSettings.appData)
373
399
  }
400
+ };
401
+ const intent = { ...solanaQuote.intent, ...overrides };
402
+ let uid = solanaQuote.uid;
403
+ let orderPda = solanaQuote.orderPda;
404
+ if (Object.keys(overrides).length > 0) {
405
+ const intentBytes = encodeOrderIntent(intent);
406
+ uid = await hashOrderIntent(intentBytes);
407
+ [orderPda] = findOrderPda(solanaQuote.programId, uid);
374
408
  }
375
409
  const instruction = buildCreateOrderInstruction({
376
410
  programId: solanaQuote.programId,
@@ -406,10 +440,26 @@ async function postSolanaSwapOrderFromQuote(quote, signAndSend, advancedSettings
406
440
  }
407
441
 
408
442
  // src/solanaTradingSdk.ts
443
+ var import_spl_token3 = require("@solana/spl-token");
444
+ var import_web37 = require("@solana/web3.js");
409
445
  var SolanaTradingSdk = class {
410
446
  constructor(options = {}) {
411
447
  this.options = options;
412
448
  }
449
+ /**
450
+ * Builds the SPL `approve` instruction that delegates `sellAmount` of `sellTokenAddress` to the
451
+ * settlement program, without sending it — see "Step 0" in the README for why this is required
452
+ * before an order on a given sell-token account can settle. Bundle it with `buildOrder`'s
453
+ * instruction in the same transaction the first time a wallet trades a given token.
454
+ */
455
+ approveCowProtocol(params) {
456
+ const owner = new import_web37.PublicKey(params.ownerAddress);
457
+ const sellMint = new import_web37.PublicKey(params.sellTokenAddress);
458
+ const tokenProgramId = params.sellTokenProgramId ? new import_web37.PublicKey(params.sellTokenProgramId) : void 0;
459
+ const sellTokenAccount = (0, import_spl_token3.getAssociatedTokenAddressSync)(sellMint, owner, false, tokenProgramId);
460
+ const delegate = getSolanaDelegateAuthority(this.options.env);
461
+ return (0, import_spl_token3.createApproveInstruction)(sellTokenAccount, delegate, owner, params.approveAmount, void 0, tokenProgramId);
462
+ }
413
463
  async getQuote(params) {
414
464
  const quote = await getSolanaQuote(params, { env: this.options.env });
415
465
  return {
package/dist/index.mjs CHANGED
@@ -24,10 +24,10 @@ function encodeOrderIntent(intent) {
24
24
  offset += 8;
25
25
  };
26
26
  writePubkey(intent.owner);
27
- writePubkey(intent.buyTokenAccount);
28
- writePubkey(intent.buyMint);
29
27
  writePubkey(intent.sellTokenAccount);
30
28
  writePubkey(intent.sellMint);
29
+ writePubkey(intent.buyTokenAccount);
30
+ writePubkey(intent.buyMint);
31
31
  writeU64LE(intent.sellAmount);
32
32
  writeU64LE(intent.buyAmount);
33
33
  view.setUint32(offset, intent.validTo, true);
@@ -189,7 +189,7 @@ async function getSolanaQuote(params, options = {}) {
189
189
  const {
190
190
  slippageBps: slippageBpsOverride,
191
191
  ownerAddress,
192
- receiverAddress,
192
+ receiverAddress = ownerAddress,
193
193
  sellTokenDecimals,
194
194
  buyTokenDecimals,
195
195
  amount,
@@ -220,7 +220,7 @@ async function getSolanaQuote(params, options = {}) {
220
220
  amount: amount.toString(),
221
221
  swapMode: kind === OrderKind2.SELL ? "ExactIn" : "ExactOut"
222
222
  });
223
- const suggestedSlippageBps = slippageBpsOverride ?? jupiterOrder.slippageBps;
223
+ const signedSlippageBps = slippageBpsOverride ?? jupiterOrder.slippageBps;
224
224
  const validTo = Math.floor(Date.now() / 1e3) + validForSeconds;
225
225
  const orderParams = {
226
226
  sellToken: sellTokenAddress,
@@ -241,7 +241,7 @@ async function getSolanaQuote(params, options = {}) {
241
241
  };
242
242
  const amountsAndCosts = getQuoteAmountsAndCosts({
243
243
  orderParams,
244
- slippagePercentBps: suggestedSlippageBps,
244
+ slippagePercentBps: signedSlippageBps,
245
245
  // TODO: implement fees
246
246
  partnerFeeBps: 0,
247
247
  protocolFeeBps: 0
@@ -298,7 +298,9 @@ async function getSolanaQuote(params, options = {}) {
298
298
  const quoteResults = {
299
299
  quoteResponse,
300
300
  amountsAndCosts,
301
- suggestedSlippageBps,
301
+ // What the quote provider suggested, never the caller's own `slippageBps`: consumers read this as a
302
+ // recommendation and would otherwise be handed their own input back as advice.
303
+ suggestedSlippageBps: jupiterOrder.slippageBps,
302
304
  tradeParameters,
303
305
  orderToSign: {},
304
306
  appDataInfo: {},
@@ -311,27 +313,49 @@ async function getSolanaQuote(params, options = {}) {
311
313
  import { SigningScheme } from "@cowprotocol/sdk-order-book";
312
314
  import { getAssociatedTokenAddressSync as getAssociatedTokenAddressSync2 } from "@solana/spl-token";
313
315
  import { PublicKey as PublicKey6 } from "@solana/web3.js";
316
+
317
+ // src/appData.ts
318
+ import { stringifyDeterministic } from "@cowprotocol/sdk-app-data";
319
+ import deepmerge from "deepmerge";
320
+ async function mergeAppData(doc, override) {
321
+ const clearedDoc = {
322
+ ...doc,
323
+ metadata: {
324
+ ...doc.metadata,
325
+ ...override.metadata?.hooks ? { hooks: {} } : {},
326
+ ...override.metadata?.userConsents ? { userConsents: [] } : {}
327
+ }
328
+ };
329
+ const merged = deepmerge(clearedDoc, override);
330
+ const fullAppData = await stringifyDeterministic(merged);
331
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(fullAppData));
332
+ return new Uint8Array(digest);
333
+ }
334
+
335
+ // src/buildSwapOrder.ts
314
336
  async function buildSolanaSwapOrder({ quoteResults, solanaQuote }, advancedSettings) {
315
- const intent = { ...solanaQuote.intent };
316
- let uid = solanaQuote.uid;
317
- let orderPda = solanaQuote.orderPda;
318
- if (advancedSettings?.quoteRequest) {
319
- const { validTo, receiver } = advancedSettings.quoteRequest;
320
- if (receiver) {
321
- intent.buyTokenAccount = getAssociatedTokenAddressSync2(
322
- intent.buyMint,
337
+ const { validTo, receiver } = advancedSettings?.quoteRequest ?? {};
338
+ const overrides = {
339
+ ...receiver && {
340
+ buyTokenAccount: getAssociatedTokenAddressSync2(
341
+ solanaQuote.intent.buyMint,
323
342
  new PublicKey6(receiver),
324
343
  false,
325
344
  solanaQuote.buyTokenProgramId
326
- );
327
- }
328
- if (validTo)
329
- intent.validTo = validTo;
330
- if (receiver || validTo) {
331
- const intentBytes = encodeOrderIntent(intent);
332
- uid = await hashOrderIntent(intentBytes);
333
- [orderPda] = findOrderPda(solanaQuote.programId, uid);
345
+ )
346
+ },
347
+ ...validTo && { validTo },
348
+ ...advancedSettings?.appData && {
349
+ appData: await mergeAppData(quoteResults.appDataInfo.doc ?? {}, advancedSettings.appData)
334
350
  }
351
+ };
352
+ const intent = { ...solanaQuote.intent, ...overrides };
353
+ let uid = solanaQuote.uid;
354
+ let orderPda = solanaQuote.orderPda;
355
+ if (Object.keys(overrides).length > 0) {
356
+ const intentBytes = encodeOrderIntent(intent);
357
+ uid = await hashOrderIntent(intentBytes);
358
+ [orderPda] = findOrderPda(solanaQuote.programId, uid);
335
359
  }
336
360
  const instruction = buildCreateOrderInstruction({
337
361
  programId: solanaQuote.programId,
@@ -367,10 +391,26 @@ async function postSolanaSwapOrderFromQuote(quote, signAndSend, advancedSettings
367
391
  }
368
392
 
369
393
  // src/solanaTradingSdk.ts
394
+ import { createApproveInstruction, getAssociatedTokenAddressSync as getAssociatedTokenAddressSync3 } from "@solana/spl-token";
395
+ import { PublicKey as PublicKey7 } from "@solana/web3.js";
370
396
  var SolanaTradingSdk = class {
371
397
  constructor(options = {}) {
372
398
  this.options = options;
373
399
  }
400
+ /**
401
+ * Builds the SPL `approve` instruction that delegates `sellAmount` of `sellTokenAddress` to the
402
+ * settlement program, without sending it — see "Step 0" in the README for why this is required
403
+ * before an order on a given sell-token account can settle. Bundle it with `buildOrder`'s
404
+ * instruction in the same transaction the first time a wallet trades a given token.
405
+ */
406
+ approveCowProtocol(params) {
407
+ const owner = new PublicKey7(params.ownerAddress);
408
+ const sellMint = new PublicKey7(params.sellTokenAddress);
409
+ const tokenProgramId = params.sellTokenProgramId ? new PublicKey7(params.sellTokenProgramId) : void 0;
410
+ const sellTokenAccount = getAssociatedTokenAddressSync3(sellMint, owner, false, tokenProgramId);
411
+ const delegate = getSolanaDelegateAuthority(this.options.env);
412
+ return createApproveInstruction(sellTokenAccount, delegate, owner, params.approveAmount, void 0, tokenProgramId);
413
+ }
374
414
  async getQuote(params) {
375
415
  const quote = await getSolanaQuote(params, { env: this.options.env });
376
416
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cowprotocol/sdk-trading-solana",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "CowProtocol Solana trading: Jupiter-sourced quotes and on-chain CreateOrder posting against the Solana settlement program",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,11 +37,13 @@
37
37
  "ts-jest": "^29.0.0"
38
38
  },
39
39
  "dependencies": {
40
- "@cowprotocol/sdk-config": "2.6.0",
41
- "@cowprotocol/sdk-order-book": "4.0.5",
42
- "@cowprotocol/sdk-trading": "2.5.0",
40
+ "@cowprotocol/sdk-app-data": "6.0.5",
41
+ "@cowprotocol/sdk-config": "2.7.0",
42
+ "@cowprotocol/sdk-order-book": "4.0.6",
43
+ "@cowprotocol/sdk-trading": "2.6.0",
43
44
  "@solana/spl-token": "0.4.14",
44
- "@solana/web3.js": "1.98.4"
45
+ "@solana/web3.js": "1.98.4",
46
+ "deepmerge": "^4.3.1"
45
47
  },
46
48
  "scripts": {
47
49
  "build": "tsup src/index.ts --format esm,cjs --dts",