@0block.io/sdk 0.1.0 → 0.2.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 +50 -4
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/idl/tenantWrapper.d.ts +166 -5
- package/dist/idl/tenantWrapper.js +371 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +13 -10
- package/dist/landing.d.ts +66 -0
- package/dist/landing.js +78 -0
- package/dist/pda.d.ts +6 -0
- package/dist/pda.js +18 -1
- package/dist/swap.js +25 -3
- package/dist/transaction.js +51 -18
- package/dist/types.d.ts +23 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@0block.io/sdk)
|
|
4
4
|
[](https://www.npmjs.com/package/@0block.io/sdk)
|
|
5
5
|
|
|
6
|
-
**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
**The SDK for 0Block's two products — independent, composable:**
|
|
7
|
+
|
|
8
|
+
| Product | What it does | Needs the other? |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| **DEX Smart Router** | Build swap transactions on-chain-routed across 60+ venues — through your tenant wrapper (your fee applies) or directly (0Block fee only) | No — submit through any RPC |
|
|
11
|
+
| **Landing Service** | Land **any** fully-signed Solana transaction: tip-gated submission forwarded to staked infrastructure and current/next leader TPUs | No — accepts any transaction |
|
|
12
|
+
|
|
13
|
+
Used together they form a complete trade pipeline: build with the router SDK,
|
|
14
|
+
sign externally, land through the service.
|
|
15
|
+
|
|
16
|
+
**Router highlights:**
|
|
10
17
|
|
|
11
18
|
- **Zero network calls at trade time.** Market context, lookup tables, and a
|
|
12
19
|
blockhash are resolved once server-side and streamed; the client assembles
|
|
@@ -14,6 +21,8 @@ accounting.
|
|
|
14
21
|
- **One signature.** The end user is the only required signer, in both modes.
|
|
15
22
|
- **Fees are on-chain, not inputs.** Tenant and platform commissions are read
|
|
16
23
|
and enforced by the programs; the SDK never accepts a fee parameter.
|
|
24
|
+
- **SOL-native UX, program-managed.** In tenant mode the wrapper wraps/unwraps
|
|
25
|
+
and cleans up token accounts on-chain — clients never touch wSOL.
|
|
17
26
|
- **External-signer native.** Returns the exact message bytes to sign, for
|
|
18
27
|
wallets, signing services, or HSMs — no `Keypair` ever touches the SDK.
|
|
19
28
|
|
|
@@ -117,6 +126,36 @@ const events = decodeSwapResultEvents(tx.meta.logMessages);
|
|
|
117
126
|
// items: [{ kind: "platform" | "tenant", bps, amount, tenantProgram }] } }]
|
|
118
127
|
```
|
|
119
128
|
|
|
129
|
+
## Landing Service (standalone)
|
|
130
|
+
|
|
131
|
+
Land any transaction — router-built or otherwise. Three rules: include the
|
|
132
|
+
tip (a plain transfer to an allowlisted account, kept as a **static** message
|
|
133
|
+
key, never behind a lookup table), use a **finalized** blockhash, and confirm
|
|
134
|
+
by **polling** (no websocket).
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { createLandingClient, withLandingTip } from "@0block.io/sdk";
|
|
138
|
+
|
|
139
|
+
// Any instructions at all — no router required:
|
|
140
|
+
const ixs = withLandingTip(myInstructions, payer, {
|
|
141
|
+
account: tipAccount, // allowlisted tip account
|
|
142
|
+
lamports: 1_000_000n, // ≥ the service minimum
|
|
143
|
+
});
|
|
144
|
+
// ...compile with a FINALIZED blockhash, sign...
|
|
145
|
+
|
|
146
|
+
const landing = createLandingClient({
|
|
147
|
+
url: "https://<landing-endpoint>",
|
|
148
|
+
apiKey: process.env.ZEROBLOCK_API_KEY,
|
|
149
|
+
});
|
|
150
|
+
const { signature, slot } = await landing.submitAndConfirm(wireBytes, readConnection, {
|
|
151
|
+
lastValidBlockHeight,
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Router-built transactions already carry the tip when you pass `tip` to
|
|
156
|
+
`buildPumpfunSwapTransaction` — hand their signed wire bytes straight to
|
|
157
|
+
`landing.sendTransaction`.
|
|
158
|
+
|
|
120
159
|
## API overview
|
|
121
160
|
|
|
122
161
|
| Runs | Function | Purpose |
|
|
@@ -135,6 +174,13 @@ exported as well.
|
|
|
135
174
|
|
|
136
175
|
## Operational notes
|
|
137
176
|
|
|
177
|
+
- **Order ids are server-authoritative in tenant mode.** The wrapper enforces
|
|
178
|
+
an exactly-once replay gate keyed by `(payer, order_id)`: a duplicate order
|
|
179
|
+
id fails at account creation and the whole swap reverts. Mint `orderId` on
|
|
180
|
+
your backend (non-zero u128, one per business order — reuse it across
|
|
181
|
+
retries) and pass it to `buildPumpfunSwapTransaction`; the SDK refuses to
|
|
182
|
+
build tenant swaps without one. Direct mode has no marker; a random id is
|
|
183
|
+
generated when omitted.
|
|
138
184
|
- **Tenant mode prerequisites.** Your wrapper must be the build in which the
|
|
139
185
|
`wrapper_config` PDA signs the router CPI, and the router's
|
|
140
186
|
`TenantConfig.tenant_authority` must equal that PDA
|
package/dist/constants.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare const FEE_MINT_ALLOWLIST: readonly string[];
|
|
|
20
20
|
export declare const PUMPFUN_PROGRAM_ID: web3.PublicKey;
|
|
21
21
|
export declare const PUMPFUN_FEE_PROGRAM_ID: web3.PublicKey;
|
|
22
22
|
export declare const SEED_WRAPPER_CONFIG: Uint8Array<ArrayBuffer>;
|
|
23
|
+
export declare const SEED_ORDER_MARKER: Uint8Array<ArrayBuffer>;
|
|
23
24
|
export declare const SEED_ROUTER_GLOBAL_CONFIG: Uint8Array<ArrayBuffer>;
|
|
24
25
|
export declare const SEED_ROUTER_TENANT: Uint8Array<ArrayBuffer>;
|
|
25
26
|
export declare const SEED_PUMPFUN_GLOBAL: Uint8Array<ArrayBuffer>;
|
package/dist/constants.js
CHANGED
|
@@ -29,6 +29,7 @@ export const PUMPFUN_PROGRAM_ID = new web3.PublicKey("6EF8rrecthR5Dkzon8Nwu78hRv
|
|
|
29
29
|
export const PUMPFUN_FEE_PROGRAM_ID = new web3.PublicKey("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ");
|
|
30
30
|
const te = new TextEncoder();
|
|
31
31
|
export const SEED_WRAPPER_CONFIG = te.encode("tenant_wrapper_config");
|
|
32
|
+
export const SEED_ORDER_MARKER = te.encode("order");
|
|
32
33
|
export const SEED_ROUTER_GLOBAL_CONFIG = te.encode("config");
|
|
33
34
|
export const SEED_ROUTER_TENANT = te.encode("tenant");
|
|
34
35
|
export const SEED_PUMPFUN_GLOBAL = te.encode("global");
|
|
@@ -74,6 +74,7 @@ export declare const TENANT_WRAPPER_IDL: {
|
|
|
74
74
|
}];
|
|
75
75
|
}, {
|
|
76
76
|
readonly name: "swap";
|
|
77
|
+
readonly docs: readonly ["Tenant swap with a hard, exactly-once-SUCCESS replay gate (R-6 / H1).", "", "The `order_marker` account is created with Anchor `init` (NOT", "`init_if_needed`) ATOMICALLY in this same instruction, BEFORE the router", "CPI runs. A second instruction with the same `(payer, order_id)` fails at", "`create_account` (\"account already in use\") and reverts the WHOLE", "instruction — including the router CPI — so a duplicate business order can", "never execute (and never pays fees) twice.", "", "INVARIANTS (off-chain encoder MUST uphold):", "- `order_id` is SERVER-AUTHORITATIVE. It is minted by the backend per", "business order and is NEVER a client-supplied value. A client that could", "choose `order_id` could grief a victim only inside its own `payer`", "namespace (see namespacing note below), but a server-authoritative id", "also guarantees that all per-gate provider-variants (0block / Jito /", "Helius / ...) of ONE business order reuse the SAME `(payer, order_id)`,", "so at most one variant can ever succeed.", "- On land-but-revert (the tx is included on-chain but the instruction", "reverts for any reason), the `init` is rolled back: the marker does NOT", "persist, the `order_id` is reclaimable, and the order may be retried with", "a FRESH durable nonce. (Durable-nonce dedup is at-most-one-LAND; this", "marker is exactly-once-SUCCESS — complementary, not redundant.)"];
|
|
77
78
|
readonly discriminator: readonly [248, 198, 158, 145, 225, 117, 135, 200];
|
|
78
79
|
readonly accounts: readonly [{
|
|
79
80
|
readonly name: "payer";
|
|
@@ -88,17 +89,33 @@ export declare const TENANT_WRAPPER_IDL: {
|
|
|
88
89
|
readonly value: readonly [116, 101, 110, 97, 110, 116, 95, 119, 114, 97, 112, 112, 101, 114, 95, 99, 111, 110, 102, 105, 103];
|
|
89
90
|
}];
|
|
90
91
|
};
|
|
92
|
+
}, {
|
|
93
|
+
readonly name: "order_marker";
|
|
94
|
+
readonly docs: readonly ["Per-order replay marker (R-6 / H1). `init` (NOT `init_if_needed`) makes a", "duplicate `(payer, order_id)` fail at `create_account` → the whole", "instruction (router CPI included) reverts.", "", "Namespaced by `payer` for griefing isolation: a third party can only ever", "init markers inside its OWN payer namespace, so it cannot pre-burn another", "user's `order_id`.", "", "ALTERNATIVE NAMESPACING: keying by `tenant_authority` instead of `payer`", "would dedup per business tenant (so the same logical order is unique across", "all of a tenant's relayer payers). We choose `payer` here because the RAX", "flow has one user-payer per order and `payer`-namespacing gives the", "strongest third-party griefing isolation; revisit if a single business", "order can legitimately arrive under multiple distinct payers."];
|
|
95
|
+
readonly writable: true;
|
|
96
|
+
readonly pda: {
|
|
97
|
+
readonly seeds: readonly [{
|
|
98
|
+
readonly kind: "const";
|
|
99
|
+
readonly value: readonly [111, 114, 100, 101, 114];
|
|
100
|
+
}, {
|
|
101
|
+
readonly kind: "account";
|
|
102
|
+
readonly path: "payer";
|
|
103
|
+
}, {
|
|
104
|
+
readonly kind: "arg";
|
|
105
|
+
readonly path: "order_id";
|
|
106
|
+
}];
|
|
107
|
+
};
|
|
91
108
|
}, {
|
|
92
109
|
readonly name: "router_program";
|
|
93
110
|
readonly docs: readonly ["Router program (hard-checked by address)"];
|
|
94
111
|
readonly address: "zeroRxfemoQCz6JT7RyShjuuRCN7VZof1WSekfXhmEn";
|
|
95
112
|
}, {
|
|
96
113
|
readonly name: "source_token_account";
|
|
97
|
-
readonly docs: readonly ["SOURCE token account (authority = payer)"];
|
|
114
|
+
readonly docs: readonly ["SOURCE token account (authority = payer).", "buys (Anchor deserializes typed accounts before the handler runs, which", "would reject a not-yet-existing ATA). The router CPI re-validates", "`token::mint = source_mint, token::authority = payer` on this account."];
|
|
98
115
|
readonly writable: true;
|
|
99
116
|
}, {
|
|
100
117
|
readonly name: "destination_token_account";
|
|
101
|
-
readonly docs: readonly ["DESTINATION token account (receives the swap output)"];
|
|
118
|
+
readonly docs: readonly ["DESTINATION token account (receives the swap output).", "M2: authority must be `payer` so output cannot be routed to an arbitrary", "same-mint account — enforced by the ROUTER's account constraints."];
|
|
102
119
|
readonly writable: true;
|
|
103
120
|
}, {
|
|
104
121
|
readonly name: "source_mint";
|
|
@@ -106,10 +123,10 @@ export declare const TENANT_WRAPPER_IDL: {
|
|
|
106
123
|
readonly name: "destination_mint";
|
|
107
124
|
}, {
|
|
108
125
|
readonly name: "global_cfg";
|
|
109
|
-
readonly docs: readonly ["Global config (router PDA)"];
|
|
126
|
+
readonly docs: readonly ["Global config (router PDA; owner/discriminator checked here, PDA seeds", "re-validated by the router). Typed so the wrapper can read the 0Block", "fee bps for exact wSOL fee-headroom sizing."];
|
|
110
127
|
}, {
|
|
111
128
|
readonly name: "tenant_cfg";
|
|
112
|
-
readonly docs: readonly ["Tenant config (router PDA)"];
|
|
129
|
+
readonly docs: readonly ["Tenant config (router PDA; owner/discriminator checked here, PDA seeds", "re-validated by the router)."];
|
|
113
130
|
}, {
|
|
114
131
|
readonly name: "zeroblock_sol_vault";
|
|
115
132
|
readonly docs: readonly ["0BLOCK fee authority (owns 0BLOCK fee token accounts)"];
|
|
@@ -138,7 +155,7 @@ export declare const TENANT_WRAPPER_IDL: {
|
|
|
138
155
|
readonly address: "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
139
156
|
}, {
|
|
140
157
|
readonly name: "system_program";
|
|
141
|
-
readonly optional:
|
|
158
|
+
readonly docs: readonly ["REQUIRED (was optional): the `order_marker` `init` needs the System", "program, and it is also forwarded to the router for fee-ATA creation."];
|
|
142
159
|
readonly address: "11111111111111111111111111111111";
|
|
143
160
|
}, {
|
|
144
161
|
readonly name: "ix_sysvar";
|
|
@@ -158,6 +175,15 @@ export declare const TENANT_WRAPPER_IDL: {
|
|
|
158
175
|
}];
|
|
159
176
|
}];
|
|
160
177
|
readonly accounts: readonly [{
|
|
178
|
+
readonly name: "GlobalConfig";
|
|
179
|
+
readonly discriminator: readonly [149, 8, 156, 202, 160, 252, 176, 217];
|
|
180
|
+
}, {
|
|
181
|
+
readonly name: "OrderMarker";
|
|
182
|
+
readonly discriminator: readonly [221, 221, 169, 152, 245, 69, 110, 108];
|
|
183
|
+
}, {
|
|
184
|
+
readonly name: "TenantConfig";
|
|
185
|
+
readonly discriminator: readonly [195, 252, 97, 104, 160, 30, 231, 112];
|
|
186
|
+
}, {
|
|
161
187
|
readonly name: "TenantWrapperConfig";
|
|
162
188
|
readonly discriminator: readonly [221, 67, 69, 41, 128, 45, 34, 31];
|
|
163
189
|
}];
|
|
@@ -169,6 +195,26 @@ export declare const TENANT_WRAPPER_IDL: {
|
|
|
169
195
|
readonly code: 6001;
|
|
170
196
|
readonly name: "InvalidAdmin";
|
|
171
197
|
readonly msg: "Admin cannot be the default pubkey";
|
|
198
|
+
}, {
|
|
199
|
+
readonly code: 6002;
|
|
200
|
+
readonly name: "ZeroOrderId";
|
|
201
|
+
readonly msg: "order_id must be non-zero";
|
|
202
|
+
}, {
|
|
203
|
+
readonly code: 6003;
|
|
204
|
+
readonly name: "MissingTokenProgram";
|
|
205
|
+
readonly msg: "token program account is required for native-SOL handling";
|
|
206
|
+
}, {
|
|
207
|
+
readonly code: 6004;
|
|
208
|
+
readonly name: "MissingAssociatedTokenProgram";
|
|
209
|
+
readonly msg: "associated token program account is required for native-SOL handling";
|
|
210
|
+
}, {
|
|
211
|
+
readonly code: 6005;
|
|
212
|
+
readonly name: "AmountOverflow";
|
|
213
|
+
readonly msg: "amount_in plus fees overflows u64";
|
|
214
|
+
}, {
|
|
215
|
+
readonly code: 6006;
|
|
216
|
+
readonly name: "InvalidTokenAccount";
|
|
217
|
+
readonly msg: "account is not a valid SPL token account";
|
|
172
218
|
}];
|
|
173
219
|
readonly types: readonly [{
|
|
174
220
|
readonly name: "Dex";
|
|
@@ -307,6 +353,81 @@ export declare const TENANT_WRAPPER_IDL: {
|
|
|
307
353
|
readonly name: "SolRfq";
|
|
308
354
|
}];
|
|
309
355
|
};
|
|
356
|
+
}, {
|
|
357
|
+
readonly name: "GlobalConfig";
|
|
358
|
+
readonly serialization: "bytemuckunsafe";
|
|
359
|
+
readonly repr: {
|
|
360
|
+
readonly kind: "c";
|
|
361
|
+
};
|
|
362
|
+
readonly type: {
|
|
363
|
+
readonly kind: "struct";
|
|
364
|
+
readonly fields: readonly [{
|
|
365
|
+
readonly name: "bump";
|
|
366
|
+
readonly docs: readonly ["PDA bump"];
|
|
367
|
+
readonly type: "u8";
|
|
368
|
+
}, {
|
|
369
|
+
readonly name: "admin";
|
|
370
|
+
readonly docs: readonly ["Program admin (controls global/tenant settings)"];
|
|
371
|
+
readonly type: "pubkey";
|
|
372
|
+
}, {
|
|
373
|
+
readonly name: "zeroblock_fee_bps";
|
|
374
|
+
readonly docs: readonly ["0BLOCK platform fee default (bps out of 10_000)"];
|
|
375
|
+
readonly type: "u16";
|
|
376
|
+
}, {
|
|
377
|
+
readonly name: "zeroblock_fee_vault";
|
|
378
|
+
readonly docs: readonly ["Fee authority that owns 0BLOCK fee token accounts"];
|
|
379
|
+
readonly type: "pubkey";
|
|
380
|
+
}, {
|
|
381
|
+
readonly name: "paused";
|
|
382
|
+
readonly docs: readonly ["A2 (remediation-2026-07-02): global instant-halt switch. `0` = not", "paused (default; matches the zeroed tail of every already-deployed", "account), `1` = paused. Plain `u8` rather than `bool` — zero_copy/Pod", "requires every byte pattern to be valid, which `bool` does not", "guarantee. Use `is_paused()`/`set_paused()`, never the raw field."];
|
|
383
|
+
readonly type: "u8";
|
|
384
|
+
}, {
|
|
385
|
+
readonly name: "spl_token_swap_allowlist";
|
|
386
|
+
readonly docs: readonly ["A1 (remediation-2026-07-02): admin-provisioned CPI-target allowlist", "for the `SplTokenSwap` (Dex=0) adapter — the router invokes", "`swap_accounts.dex_program_id` with the `0block_sa` PDA as signer on", "hop ≥ 1, so an unconstrained target is a confused-deputy sweep of any", "PDA-held funds. Unlike the other ~60 adapters this venue has no", "single hardcoded upstream program id (multiple live forks share the", "same instruction interface), so the allowed set is admin-configured", "instead of a compile-time const. Unset slots are `Pubkey::default()`", "and never match; an all-default allowlist rejects every CPI (fail", "closed) until an admin provisions it via `set_spl_token_swap_allowlist`", "(see `// TODO(remediation-2026-07-02):` in `adapters/spl_token_swap.rs`", "— provisioning real venue ids is a deploy-config/operator action, out", "of scope for this remediation pass, same as the plan's other", "\"provisioning\" items)."];
|
|
387
|
+
readonly type: {
|
|
388
|
+
readonly array: readonly ["pubkey", 2];
|
|
389
|
+
};
|
|
390
|
+
}, {
|
|
391
|
+
readonly name: "stable_swap_allowlist";
|
|
392
|
+
readonly docs: readonly ["A1 (remediation-2026-07-02): same contract as", "`spl_token_swap_allowlist`, for the `StableSwap` (Dex=1) adapter."];
|
|
393
|
+
readonly type: {
|
|
394
|
+
readonly array: readonly ["pubkey", 2];
|
|
395
|
+
};
|
|
396
|
+
}, {
|
|
397
|
+
readonly name: "padding";
|
|
398
|
+
readonly docs: readonly ["Reserved to keep struct size stable for future upgrades. Shrunk from", "the original 160 bytes by exactly the width of the fields above (see", "`REMAINING_PADDING_BYTES`) so total account size is unchanged."];
|
|
399
|
+
readonly type: {
|
|
400
|
+
readonly array: readonly ["u8", 31];
|
|
401
|
+
};
|
|
402
|
+
}];
|
|
403
|
+
};
|
|
404
|
+
}, {
|
|
405
|
+
readonly name: "OrderMarker";
|
|
406
|
+
readonly docs: readonly ["Per-order replay marker (R-6 / H1). Audit-only fields — the account's", "EXISTENCE under seeds `[b\"order\", payer, order_id_le]` IS the dedup signal."];
|
|
407
|
+
readonly type: {
|
|
408
|
+
readonly kind: "struct";
|
|
409
|
+
readonly fields: readonly [{
|
|
410
|
+
readonly name: "order_id";
|
|
411
|
+
readonly docs: readonly ["The server-authoritative business order id this marker pins."];
|
|
412
|
+
readonly type: "u128";
|
|
413
|
+
}, {
|
|
414
|
+
readonly name: "authority";
|
|
415
|
+
readonly docs: readonly ["The payer namespace this marker was created under (== `payer`)."];
|
|
416
|
+
readonly type: "pubkey";
|
|
417
|
+
}, {
|
|
418
|
+
readonly name: "slot";
|
|
419
|
+
readonly docs: readonly ["Slot at creation (forensics only)."];
|
|
420
|
+
readonly type: "u64";
|
|
421
|
+
}, {
|
|
422
|
+
readonly name: "unix_timestamp";
|
|
423
|
+
readonly docs: readonly ["Unix timestamp at creation (forensics only)."];
|
|
424
|
+
readonly type: "i64";
|
|
425
|
+
}, {
|
|
426
|
+
readonly name: "bump";
|
|
427
|
+
readonly docs: readonly ["PDA bump."];
|
|
428
|
+
readonly type: "u8";
|
|
429
|
+
}];
|
|
430
|
+
};
|
|
310
431
|
}, {
|
|
311
432
|
readonly name: "Route";
|
|
312
433
|
readonly type: {
|
|
@@ -395,6 +516,46 @@ export declare const TENANT_WRAPPER_IDL: {
|
|
|
395
516
|
};
|
|
396
517
|
}];
|
|
397
518
|
};
|
|
519
|
+
}, {
|
|
520
|
+
readonly name: "TenantConfig";
|
|
521
|
+
readonly docs: readonly ["----------------------------------------------------------------------------", "TenantConfig (+ strict wrapper enforcement)", "----------------------------------------------------------------------------"];
|
|
522
|
+
readonly type: {
|
|
523
|
+
readonly kind: "struct";
|
|
524
|
+
readonly fields: readonly [{
|
|
525
|
+
readonly name: "bump";
|
|
526
|
+
readonly type: "u8";
|
|
527
|
+
}, {
|
|
528
|
+
readonly name: "tenant_authority";
|
|
529
|
+
readonly docs: readonly ["Signer (EOA or PDA) authorized to act for this tenant"];
|
|
530
|
+
readonly type: "pubkey";
|
|
531
|
+
}, {
|
|
532
|
+
readonly name: "tenant_wrapper_program";
|
|
533
|
+
readonly docs: readonly ["Wrapper program id used to validate tenant-mode calls."];
|
|
534
|
+
readonly type: "pubkey";
|
|
535
|
+
}, {
|
|
536
|
+
readonly name: "zeroblock_fee_bps";
|
|
537
|
+
readonly docs: readonly ["0BLOCK fee (bps out of 10_000) for this tenant. Use 0 to fall back to global default."];
|
|
538
|
+
readonly type: "u16";
|
|
539
|
+
}, {
|
|
540
|
+
readonly name: "tenant_fee_vault";
|
|
541
|
+
readonly docs: readonly ["Fee authority that owns tenant fee token accounts"];
|
|
542
|
+
readonly type: "pubkey";
|
|
543
|
+
}, {
|
|
544
|
+
readonly name: "whitelisted";
|
|
545
|
+
readonly docs: readonly ["Whether this tenant is active/whitelisted"];
|
|
546
|
+
readonly type: "bool";
|
|
547
|
+
}, {
|
|
548
|
+
readonly name: "max_total_fee_bps";
|
|
549
|
+
readonly docs: readonly ["Optional per-tenant cap: (tenant_fee_bps + zeroblock_fee_bps) must be <= this if > 0.", "tenant_fee_bps is supplied per swap."];
|
|
550
|
+
readonly type: "u16";
|
|
551
|
+
}, {
|
|
552
|
+
readonly name: "_reserved";
|
|
553
|
+
readonly docs: readonly ["Reserved for upgrades (fixed size)"];
|
|
554
|
+
readonly type: {
|
|
555
|
+
readonly array: readonly ["u8", 64];
|
|
556
|
+
};
|
|
557
|
+
}];
|
|
558
|
+
};
|
|
398
559
|
}, {
|
|
399
560
|
readonly name: "TenantWrapperConfig";
|
|
400
561
|
readonly type: {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Vendored from the tenant-wrapper build of nyx-solana-dex-router (target/idl/zeroblock_tenant_wrapper.json).
|
|
2
2
|
// The instruction layout is identical for every tenant wrapper deployment; the program
|
|
3
3
|
// address is per-tenant and intentionally stripped — pass wrapperProgramId at init.
|
|
4
|
-
//
|
|
4
|
+
// swap: order_marker replay gate (server-authoritative non-zero order_id); the
|
|
5
|
+
// wrapper_config PDA signs the router CPI; native-SOL wrap/fee-headroom/unwrap/
|
|
6
|
+
// ATA-cleanup are PROGRAM-MANAGED — clients add no wSOL instructions.
|
|
5
7
|
export const TENANT_WRAPPER_IDL = {
|
|
6
8
|
"address": "11111111111111111111111111111111",
|
|
7
9
|
"metadata": {
|
|
@@ -201,6 +203,30 @@ export const TENANT_WRAPPER_IDL = {
|
|
|
201
203
|
},
|
|
202
204
|
{
|
|
203
205
|
"name": "swap",
|
|
206
|
+
"docs": [
|
|
207
|
+
"Tenant swap with a hard, exactly-once-SUCCESS replay gate (R-6 / H1).",
|
|
208
|
+
"",
|
|
209
|
+
"The `order_marker` account is created with Anchor `init` (NOT",
|
|
210
|
+
"`init_if_needed`) ATOMICALLY in this same instruction, BEFORE the router",
|
|
211
|
+
"CPI runs. A second instruction with the same `(payer, order_id)` fails at",
|
|
212
|
+
"`create_account` (\"account already in use\") and reverts the WHOLE",
|
|
213
|
+
"instruction — including the router CPI — so a duplicate business order can",
|
|
214
|
+
"never execute (and never pays fees) twice.",
|
|
215
|
+
"",
|
|
216
|
+
"INVARIANTS (off-chain encoder MUST uphold):",
|
|
217
|
+
"- `order_id` is SERVER-AUTHORITATIVE. It is minted by the backend per",
|
|
218
|
+
"business order and is NEVER a client-supplied value. A client that could",
|
|
219
|
+
"choose `order_id` could grief a victim only inside its own `payer`",
|
|
220
|
+
"namespace (see namespacing note below), but a server-authoritative id",
|
|
221
|
+
"also guarantees that all per-gate provider-variants (0block / Jito /",
|
|
222
|
+
"Helius / ...) of ONE business order reuse the SAME `(payer, order_id)`,",
|
|
223
|
+
"so at most one variant can ever succeed.",
|
|
224
|
+
"- On land-but-revert (the tx is included on-chain but the instruction",
|
|
225
|
+
"reverts for any reason), the `init` is rolled back: the marker does NOT",
|
|
226
|
+
"persist, the `order_id` is reclaimable, and the order may be retried with",
|
|
227
|
+
"a FRESH durable nonce. (Durable-nonce dedup is at-most-one-LAND; this",
|
|
228
|
+
"marker is exactly-once-SUCCESS — complementary, not redundant.)"
|
|
229
|
+
],
|
|
204
230
|
"discriminator": [
|
|
205
231
|
248,
|
|
206
232
|
198,
|
|
@@ -253,6 +279,48 @@ export const TENANT_WRAPPER_IDL = {
|
|
|
253
279
|
]
|
|
254
280
|
}
|
|
255
281
|
},
|
|
282
|
+
{
|
|
283
|
+
"name": "order_marker",
|
|
284
|
+
"docs": [
|
|
285
|
+
"Per-order replay marker (R-6 / H1). `init` (NOT `init_if_needed`) makes a",
|
|
286
|
+
"duplicate `(payer, order_id)` fail at `create_account` → the whole",
|
|
287
|
+
"instruction (router CPI included) reverts.",
|
|
288
|
+
"",
|
|
289
|
+
"Namespaced by `payer` for griefing isolation: a third party can only ever",
|
|
290
|
+
"init markers inside its OWN payer namespace, so it cannot pre-burn another",
|
|
291
|
+
"user's `order_id`.",
|
|
292
|
+
"",
|
|
293
|
+
"ALTERNATIVE NAMESPACING: keying by `tenant_authority` instead of `payer`",
|
|
294
|
+
"would dedup per business tenant (so the same logical order is unique across",
|
|
295
|
+
"all of a tenant's relayer payers). We choose `payer` here because the RAX",
|
|
296
|
+
"flow has one user-payer per order and `payer`-namespacing gives the",
|
|
297
|
+
"strongest third-party griefing isolation; revisit if a single business",
|
|
298
|
+
"order can legitimately arrive under multiple distinct payers."
|
|
299
|
+
],
|
|
300
|
+
"writable": true,
|
|
301
|
+
"pda": {
|
|
302
|
+
"seeds": [
|
|
303
|
+
{
|
|
304
|
+
"kind": "const",
|
|
305
|
+
"value": [
|
|
306
|
+
111,
|
|
307
|
+
114,
|
|
308
|
+
100,
|
|
309
|
+
101,
|
|
310
|
+
114
|
|
311
|
+
]
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"kind": "account",
|
|
315
|
+
"path": "payer"
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"kind": "arg",
|
|
319
|
+
"path": "order_id"
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
},
|
|
256
324
|
{
|
|
257
325
|
"name": "router_program",
|
|
258
326
|
"docs": [
|
|
@@ -263,14 +331,19 @@ export const TENANT_WRAPPER_IDL = {
|
|
|
263
331
|
{
|
|
264
332
|
"name": "source_token_account",
|
|
265
333
|
"docs": [
|
|
266
|
-
"SOURCE token account (authority = payer)"
|
|
334
|
+
"SOURCE token account (authority = payer).",
|
|
335
|
+
"buys (Anchor deserializes typed accounts before the handler runs, which",
|
|
336
|
+
"would reject a not-yet-existing ATA). The router CPI re-validates",
|
|
337
|
+
"`token::mint = source_mint, token::authority = payer` on this account."
|
|
267
338
|
],
|
|
268
339
|
"writable": true
|
|
269
340
|
},
|
|
270
341
|
{
|
|
271
342
|
"name": "destination_token_account",
|
|
272
343
|
"docs": [
|
|
273
|
-
"DESTINATION token account (receives the swap output)"
|
|
344
|
+
"DESTINATION token account (receives the swap output).",
|
|
345
|
+
"M2: authority must be `payer` so output cannot be routed to an arbitrary",
|
|
346
|
+
"same-mint account — enforced by the ROUTER's account constraints."
|
|
274
347
|
],
|
|
275
348
|
"writable": true
|
|
276
349
|
},
|
|
@@ -283,13 +356,16 @@ export const TENANT_WRAPPER_IDL = {
|
|
|
283
356
|
{
|
|
284
357
|
"name": "global_cfg",
|
|
285
358
|
"docs": [
|
|
286
|
-
"Global config (router PDA
|
|
359
|
+
"Global config (router PDA; owner/discriminator checked here, PDA seeds",
|
|
360
|
+
"re-validated by the router). Typed so the wrapper can read the 0Block",
|
|
361
|
+
"fee bps for exact wSOL fee-headroom sizing."
|
|
287
362
|
]
|
|
288
363
|
},
|
|
289
364
|
{
|
|
290
365
|
"name": "tenant_cfg",
|
|
291
366
|
"docs": [
|
|
292
|
-
"Tenant config (router PDA
|
|
367
|
+
"Tenant config (router PDA; owner/discriminator checked here, PDA seeds",
|
|
368
|
+
"re-validated by the router)."
|
|
293
369
|
]
|
|
294
370
|
},
|
|
295
371
|
{
|
|
@@ -335,7 +411,10 @@ export const TENANT_WRAPPER_IDL = {
|
|
|
335
411
|
},
|
|
336
412
|
{
|
|
337
413
|
"name": "system_program",
|
|
338
|
-
"
|
|
414
|
+
"docs": [
|
|
415
|
+
"REQUIRED (was optional): the `order_marker` `init` needs the System",
|
|
416
|
+
"program, and it is also forwarded to the router for fee-ATA creation."
|
|
417
|
+
],
|
|
339
418
|
"address": "11111111111111111111111111111111"
|
|
340
419
|
},
|
|
341
420
|
{
|
|
@@ -363,6 +442,45 @@ export const TENANT_WRAPPER_IDL = {
|
|
|
363
442
|
}
|
|
364
443
|
],
|
|
365
444
|
"accounts": [
|
|
445
|
+
{
|
|
446
|
+
"name": "GlobalConfig",
|
|
447
|
+
"discriminator": [
|
|
448
|
+
149,
|
|
449
|
+
8,
|
|
450
|
+
156,
|
|
451
|
+
202,
|
|
452
|
+
160,
|
|
453
|
+
252,
|
|
454
|
+
176,
|
|
455
|
+
217
|
|
456
|
+
]
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
"name": "OrderMarker",
|
|
460
|
+
"discriminator": [
|
|
461
|
+
221,
|
|
462
|
+
221,
|
|
463
|
+
169,
|
|
464
|
+
152,
|
|
465
|
+
245,
|
|
466
|
+
69,
|
|
467
|
+
110,
|
|
468
|
+
108
|
|
469
|
+
]
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"name": "TenantConfig",
|
|
473
|
+
"discriminator": [
|
|
474
|
+
195,
|
|
475
|
+
252,
|
|
476
|
+
97,
|
|
477
|
+
104,
|
|
478
|
+
160,
|
|
479
|
+
30,
|
|
480
|
+
231,
|
|
481
|
+
112
|
|
482
|
+
]
|
|
483
|
+
},
|
|
366
484
|
{
|
|
367
485
|
"name": "TenantWrapperConfig",
|
|
368
486
|
"discriminator": [
|
|
@@ -387,6 +505,31 @@ export const TENANT_WRAPPER_IDL = {
|
|
|
387
505
|
"code": 6001,
|
|
388
506
|
"name": "InvalidAdmin",
|
|
389
507
|
"msg": "Admin cannot be the default pubkey"
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
"code": 6002,
|
|
511
|
+
"name": "ZeroOrderId",
|
|
512
|
+
"msg": "order_id must be non-zero"
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
"code": 6003,
|
|
516
|
+
"name": "MissingTokenProgram",
|
|
517
|
+
"msg": "token program account is required for native-SOL handling"
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
"code": 6004,
|
|
521
|
+
"name": "MissingAssociatedTokenProgram",
|
|
522
|
+
"msg": "associated token program account is required for native-SOL handling"
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"code": 6005,
|
|
526
|
+
"name": "AmountOverflow",
|
|
527
|
+
"msg": "amount_in plus fees overflows u64"
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
"code": 6006,
|
|
531
|
+
"name": "InvalidTokenAccount",
|
|
532
|
+
"msg": "account is not a valid SPL token account"
|
|
390
533
|
}
|
|
391
534
|
],
|
|
392
535
|
"types": [
|
|
@@ -596,6 +739,156 @@ export const TENANT_WRAPPER_IDL = {
|
|
|
596
739
|
]
|
|
597
740
|
}
|
|
598
741
|
},
|
|
742
|
+
{
|
|
743
|
+
"name": "GlobalConfig",
|
|
744
|
+
"serialization": "bytemuckunsafe",
|
|
745
|
+
"repr": {
|
|
746
|
+
"kind": "c"
|
|
747
|
+
},
|
|
748
|
+
"type": {
|
|
749
|
+
"kind": "struct",
|
|
750
|
+
"fields": [
|
|
751
|
+
{
|
|
752
|
+
"name": "bump",
|
|
753
|
+
"docs": [
|
|
754
|
+
"PDA bump"
|
|
755
|
+
],
|
|
756
|
+
"type": "u8"
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
"name": "admin",
|
|
760
|
+
"docs": [
|
|
761
|
+
"Program admin (controls global/tenant settings)"
|
|
762
|
+
],
|
|
763
|
+
"type": "pubkey"
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
"name": "zeroblock_fee_bps",
|
|
767
|
+
"docs": [
|
|
768
|
+
"0BLOCK platform fee default (bps out of 10_000)"
|
|
769
|
+
],
|
|
770
|
+
"type": "u16"
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
"name": "zeroblock_fee_vault",
|
|
774
|
+
"docs": [
|
|
775
|
+
"Fee authority that owns 0BLOCK fee token accounts"
|
|
776
|
+
],
|
|
777
|
+
"type": "pubkey"
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
"name": "paused",
|
|
781
|
+
"docs": [
|
|
782
|
+
"A2 (remediation-2026-07-02): global instant-halt switch. `0` = not",
|
|
783
|
+
"paused (default; matches the zeroed tail of every already-deployed",
|
|
784
|
+
"account), `1` = paused. Plain `u8` rather than `bool` — zero_copy/Pod",
|
|
785
|
+
"requires every byte pattern to be valid, which `bool` does not",
|
|
786
|
+
"guarantee. Use `is_paused()`/`set_paused()`, never the raw field."
|
|
787
|
+
],
|
|
788
|
+
"type": "u8"
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
"name": "spl_token_swap_allowlist",
|
|
792
|
+
"docs": [
|
|
793
|
+
"A1 (remediation-2026-07-02): admin-provisioned CPI-target allowlist",
|
|
794
|
+
"for the `SplTokenSwap` (Dex=0) adapter — the router invokes",
|
|
795
|
+
"`swap_accounts.dex_program_id` with the `0block_sa` PDA as signer on",
|
|
796
|
+
"hop ≥ 1, so an unconstrained target is a confused-deputy sweep of any",
|
|
797
|
+
"PDA-held funds. Unlike the other ~60 adapters this venue has no",
|
|
798
|
+
"single hardcoded upstream program id (multiple live forks share the",
|
|
799
|
+
"same instruction interface), so the allowed set is admin-configured",
|
|
800
|
+
"instead of a compile-time const. Unset slots are `Pubkey::default()`",
|
|
801
|
+
"and never match; an all-default allowlist rejects every CPI (fail",
|
|
802
|
+
"closed) until an admin provisions it via `set_spl_token_swap_allowlist`",
|
|
803
|
+
"(see `// TODO(remediation-2026-07-02):` in `adapters/spl_token_swap.rs`",
|
|
804
|
+
"— provisioning real venue ids is a deploy-config/operator action, out",
|
|
805
|
+
"of scope for this remediation pass, same as the plan's other",
|
|
806
|
+
"\"provisioning\" items)."
|
|
807
|
+
],
|
|
808
|
+
"type": {
|
|
809
|
+
"array": [
|
|
810
|
+
"pubkey",
|
|
811
|
+
2
|
|
812
|
+
]
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
"name": "stable_swap_allowlist",
|
|
817
|
+
"docs": [
|
|
818
|
+
"A1 (remediation-2026-07-02): same contract as",
|
|
819
|
+
"`spl_token_swap_allowlist`, for the `StableSwap` (Dex=1) adapter."
|
|
820
|
+
],
|
|
821
|
+
"type": {
|
|
822
|
+
"array": [
|
|
823
|
+
"pubkey",
|
|
824
|
+
2
|
|
825
|
+
]
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
"name": "padding",
|
|
830
|
+
"docs": [
|
|
831
|
+
"Reserved to keep struct size stable for future upgrades. Shrunk from",
|
|
832
|
+
"the original 160 bytes by exactly the width of the fields above (see",
|
|
833
|
+
"`REMAINING_PADDING_BYTES`) so total account size is unchanged."
|
|
834
|
+
],
|
|
835
|
+
"type": {
|
|
836
|
+
"array": [
|
|
837
|
+
"u8",
|
|
838
|
+
31
|
|
839
|
+
]
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
]
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
"name": "OrderMarker",
|
|
847
|
+
"docs": [
|
|
848
|
+
"Per-order replay marker (R-6 / H1). Audit-only fields — the account's",
|
|
849
|
+
"EXISTENCE under seeds `[b\"order\", payer, order_id_le]` IS the dedup signal."
|
|
850
|
+
],
|
|
851
|
+
"type": {
|
|
852
|
+
"kind": "struct",
|
|
853
|
+
"fields": [
|
|
854
|
+
{
|
|
855
|
+
"name": "order_id",
|
|
856
|
+
"docs": [
|
|
857
|
+
"The server-authoritative business order id this marker pins."
|
|
858
|
+
],
|
|
859
|
+
"type": "u128"
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
"name": "authority",
|
|
863
|
+
"docs": [
|
|
864
|
+
"The payer namespace this marker was created under (== `payer`)."
|
|
865
|
+
],
|
|
866
|
+
"type": "pubkey"
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
"name": "slot",
|
|
870
|
+
"docs": [
|
|
871
|
+
"Slot at creation (forensics only)."
|
|
872
|
+
],
|
|
873
|
+
"type": "u64"
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
"name": "unix_timestamp",
|
|
877
|
+
"docs": [
|
|
878
|
+
"Unix timestamp at creation (forensics only)."
|
|
879
|
+
],
|
|
880
|
+
"type": "i64"
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
"name": "bump",
|
|
884
|
+
"docs": [
|
|
885
|
+
"PDA bump."
|
|
886
|
+
],
|
|
887
|
+
"type": "u8"
|
|
888
|
+
}
|
|
889
|
+
]
|
|
890
|
+
}
|
|
891
|
+
},
|
|
599
892
|
{
|
|
600
893
|
"name": "Route",
|
|
601
894
|
"type": {
|
|
@@ -707,6 +1000,78 @@ export const TENANT_WRAPPER_IDL = {
|
|
|
707
1000
|
]
|
|
708
1001
|
}
|
|
709
1002
|
},
|
|
1003
|
+
{
|
|
1004
|
+
"name": "TenantConfig",
|
|
1005
|
+
"docs": [
|
|
1006
|
+
"----------------------------------------------------------------------------",
|
|
1007
|
+
"TenantConfig (+ strict wrapper enforcement)",
|
|
1008
|
+
"----------------------------------------------------------------------------"
|
|
1009
|
+
],
|
|
1010
|
+
"type": {
|
|
1011
|
+
"kind": "struct",
|
|
1012
|
+
"fields": [
|
|
1013
|
+
{
|
|
1014
|
+
"name": "bump",
|
|
1015
|
+
"type": "u8"
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
"name": "tenant_authority",
|
|
1019
|
+
"docs": [
|
|
1020
|
+
"Signer (EOA or PDA) authorized to act for this tenant"
|
|
1021
|
+
],
|
|
1022
|
+
"type": "pubkey"
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
"name": "tenant_wrapper_program",
|
|
1026
|
+
"docs": [
|
|
1027
|
+
"Wrapper program id used to validate tenant-mode calls."
|
|
1028
|
+
],
|
|
1029
|
+
"type": "pubkey"
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
"name": "zeroblock_fee_bps",
|
|
1033
|
+
"docs": [
|
|
1034
|
+
"0BLOCK fee (bps out of 10_000) for this tenant. Use 0 to fall back to global default."
|
|
1035
|
+
],
|
|
1036
|
+
"type": "u16"
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
"name": "tenant_fee_vault",
|
|
1040
|
+
"docs": [
|
|
1041
|
+
"Fee authority that owns tenant fee token accounts"
|
|
1042
|
+
],
|
|
1043
|
+
"type": "pubkey"
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
"name": "whitelisted",
|
|
1047
|
+
"docs": [
|
|
1048
|
+
"Whether this tenant is active/whitelisted"
|
|
1049
|
+
],
|
|
1050
|
+
"type": "bool"
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
"name": "max_total_fee_bps",
|
|
1054
|
+
"docs": [
|
|
1055
|
+
"Optional per-tenant cap: (tenant_fee_bps + zeroblock_fee_bps) must be <= this if > 0.",
|
|
1056
|
+
"tenant_fee_bps is supplied per swap."
|
|
1057
|
+
],
|
|
1058
|
+
"type": "u16"
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
"name": "_reserved",
|
|
1062
|
+
"docs": [
|
|
1063
|
+
"Reserved for upgrades (fixed size)"
|
|
1064
|
+
],
|
|
1065
|
+
"type": {
|
|
1066
|
+
"array": [
|
|
1067
|
+
"u8",
|
|
1068
|
+
64
|
|
1069
|
+
]
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
]
|
|
1073
|
+
}
|
|
1074
|
+
},
|
|
710
1075
|
{
|
|
711
1076
|
"name": "TenantWrapperConfig",
|
|
712
1077
|
"type": {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from "./constants.js";
|
|
2
2
|
export * from "./types.js";
|
|
3
3
|
export { createContext, type CreateContextParams, type TenantInit, type ZeroBlockContext, } from "./context.js";
|
|
4
|
-
export {
|
|
4
|
+
export { buildSendTransactionRequest, createLandingClient, withLandingTip, type LandingClient, type LandingServiceConfig, type SendTransactionOptions, } from "./landing.js";
|
|
5
|
+
export { toPk, deriveAta, deriveOrderMarker, deriveWrapperConfig, deriveRouterGlobalConfig, deriveRouterTenantConfig, derivePumpfunGlobal, derivePumpfunBondingCurve, derivePumpfunBondingCurveV2, derivePumpfunEventAuthority, derivePumpfunCreatorVault, derivePumpfunGlobalVolumeAccumulator, derivePumpfunUserVolumeAccumulator, derivePumpfunFeeConfig, } from "./pda.js";
|
|
5
6
|
export { type AccountFetcher, connectionFetcher, resolveTokenProgram, resolvePumpfunMarketContext, fetchPumpfunGlobalFeeRecipient, fetchPumpfunBondingCurveCreator, fetchPumpfunBuybackFeeRecipients, } from "./fetch.js";
|
|
6
7
|
export { buildPumpfunRemainingAccounts, type PumpfunRemainingAccountsParams, } from "./venues/pumpfun.js";
|
|
7
8
|
export { buildPumpfunSwapInstruction, decodeSwapData, randomOrderId, resolveFeeMint, type SwapInstructionResult, } from "./swap.js";
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
// @0block/sdk —
|
|
1
|
+
// @0block.io/sdk — SDK for 0Block's two independent products:
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// 1. DEX SMART ROUTER — build swap transactions (tenant-wrapped or direct).
|
|
4
|
+
// createContext, applyPreset, buildPumpfunSwapTransaction,
|
|
5
|
+
// resolvePumpfunMarketContext, ALT helpers, decodeSwapResultEvents.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// 2. LANDING SERVICE — land ANY fully-signed Solana transaction (tip-gated
|
|
8
|
+
// submission to staked infra + leader TPUs). createLandingClient,
|
|
9
|
+
// withLandingTip, buildTipInstruction, maskTipAccountInTables.
|
|
10
|
+
//
|
|
11
|
+
// Neither depends on the other: router transactions can be submitted through
|
|
12
|
+
// any RPC, and the landing service accepts any transaction. They compose
|
|
13
|
+
// naturally when used together.
|
|
12
14
|
export * from "./constants.js";
|
|
13
15
|
export * from "./types.js";
|
|
14
16
|
export { createContext, } from "./context.js";
|
|
15
|
-
export {
|
|
17
|
+
export { buildSendTransactionRequest, createLandingClient, withLandingTip, } from "./landing.js";
|
|
18
|
+
export { toPk, deriveAta, deriveOrderMarker, deriveWrapperConfig, deriveRouterGlobalConfig, deriveRouterTenantConfig, derivePumpfunGlobal, derivePumpfunBondingCurve, derivePumpfunBondingCurveV2, derivePumpfunEventAuthority, derivePumpfunCreatorVault, derivePumpfunGlobalVolumeAccumulator, derivePumpfunUserVolumeAccumulator, derivePumpfunFeeConfig, } from "./pda.js";
|
|
16
19
|
export { connectionFetcher, resolveTokenProgram, resolvePumpfunMarketContext, fetchPumpfunGlobalFeeRecipient, fetchPumpfunBondingCurveCreator, fetchPumpfunBuybackFeeRecipients, } from "./fetch.js";
|
|
17
20
|
export { buildPumpfunRemainingAccounts, } from "./venues/pumpfun.js";
|
|
18
21
|
export { buildPumpfunSwapInstruction, decodeSwapData, randomOrderId, resolveFeeMint, } from "./swap.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as web3 from "@solana/web3.js";
|
|
2
|
+
import type { Address, TipParams } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* 0Block Landing service — a standalone product, independent of the DEX
|
|
5
|
+
* router. It lands ANY fully-signed Solana transaction: JSON-RPC
|
|
6
|
+
* `sendTransaction` gated by a tip transfer, forwarded to staked
|
|
7
|
+
* infrastructure and current/next leader TPUs.
|
|
8
|
+
*
|
|
9
|
+
* Contract (independent of what the transaction does):
|
|
10
|
+
* - include a SystemProgram transfer ≥ the minimum tip to an allowlisted tip
|
|
11
|
+
* account, kept as a STATIC message key (never behind an address lookup
|
|
12
|
+
* table — the service verifies raw bytes; see maskTipAccountInTables),
|
|
13
|
+
* - use a FINALIZED blockhash (the service submits via different nodes than
|
|
14
|
+
* the one you read from),
|
|
15
|
+
* - confirm by polling: the service does not serve websockets.
|
|
16
|
+
*/
|
|
17
|
+
export interface LandingServiceConfig {
|
|
18
|
+
/** Landing service endpoint, e.g. "https://gateway.0block.io". */
|
|
19
|
+
url: string;
|
|
20
|
+
/** API key, sent as X-API-Key. */
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
/** Injectable fetch (testing / non-standard runtimes). Defaults to global fetch. */
|
|
23
|
+
fetchImpl?: typeof fetch;
|
|
24
|
+
}
|
|
25
|
+
export interface SendTransactionOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Skip the upstream's preflight simulation (default true — the low-latency
|
|
28
|
+
* path; a still-propagating blockhash can fail simulation spuriously).
|
|
29
|
+
*/
|
|
30
|
+
skipPreflight?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** The exact HTTP request the Landing service expects (pure; useful for tests and custom transports). */
|
|
33
|
+
export declare function buildSendTransactionRequest(config: Pick<LandingServiceConfig, "url" | "apiKey">, wireTransaction: Uint8Array, options?: SendTransactionOptions): {
|
|
34
|
+
url: string;
|
|
35
|
+
method: "POST";
|
|
36
|
+
headers: Record<string, string>;
|
|
37
|
+
body: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Prepend the landing tip to an instruction list for ANY transaction (router
|
|
41
|
+
* swap or otherwise). The swap builder does this internally when given `tip`;
|
|
42
|
+
* use this for arbitrary transactions you land through the service.
|
|
43
|
+
*/
|
|
44
|
+
export declare function withLandingTip(instructions: web3.TransactionInstruction[], payer: Address, tip: TipParams | {
|
|
45
|
+
account: Address;
|
|
46
|
+
lamports?: bigint;
|
|
47
|
+
}): web3.TransactionInstruction[];
|
|
48
|
+
export interface LandingClient {
|
|
49
|
+
/** Submit wire bytes (fully signed). Resolves to the transaction signature (submission ack, NOT confirmation). */
|
|
50
|
+
sendTransaction(wireTransaction: Uint8Array, options?: SendTransactionOptions): Promise<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Submit, then poll the given read connection until the signature confirms,
|
|
53
|
+
* errors, or the blockhash lapses. The read RPC is separate from the
|
|
54
|
+
* landing endpoint by design.
|
|
55
|
+
*/
|
|
56
|
+
submitAndConfirm(wireTransaction: Uint8Array, readConnection: web3.Connection, params: {
|
|
57
|
+
lastValidBlockHeight: number;
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
pollIntervalMs?: number;
|
|
60
|
+
}, options?: SendTransactionOptions): Promise<{
|
|
61
|
+
signature: string;
|
|
62
|
+
slot: number;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
/** Client for the Landing service. Usable with or without the DEX router. */
|
|
66
|
+
export declare function createLandingClient(config: LandingServiceConfig): LandingClient;
|
package/dist/landing.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DEFAULT_TIP_LAMPORTS } from "./constants.js";
|
|
2
|
+
import { buildTipInstruction } from "./tip.js";
|
|
3
|
+
import { toPk } from "./pda.js";
|
|
4
|
+
/** The exact HTTP request the Landing service expects (pure; useful for tests and custom transports). */
|
|
5
|
+
export function buildSendTransactionRequest(config, wireTransaction, options) {
|
|
6
|
+
const headers = { "Content-Type": "application/json" };
|
|
7
|
+
if (config.apiKey)
|
|
8
|
+
headers["X-API-Key"] = config.apiKey;
|
|
9
|
+
return {
|
|
10
|
+
url: config.url,
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers,
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
jsonrpc: "2.0",
|
|
15
|
+
id: 1,
|
|
16
|
+
method: "sendTransaction",
|
|
17
|
+
params: [
|
|
18
|
+
Buffer.from(wireTransaction).toString("base64"),
|
|
19
|
+
{ encoding: "base64", skipPreflight: options?.skipPreflight ?? true },
|
|
20
|
+
],
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Prepend the landing tip to an instruction list for ANY transaction (router
|
|
26
|
+
* swap or otherwise). The swap builder does this internally when given `tip`;
|
|
27
|
+
* use this for arbitrary transactions you land through the service.
|
|
28
|
+
*/
|
|
29
|
+
export function withLandingTip(instructions, payer, tip) {
|
|
30
|
+
const lamports = tip.lamports ?? DEFAULT_TIP_LAMPORTS;
|
|
31
|
+
return [
|
|
32
|
+
buildTipInstruction(toPk(payer), { account: tip.account, lamports }),
|
|
33
|
+
...instructions,
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
/** Client for the Landing service. Usable with or without the DEX router. */
|
|
37
|
+
export function createLandingClient(config) {
|
|
38
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
39
|
+
async function sendTransaction(wireTransaction, options) {
|
|
40
|
+
const req = buildSendTransactionRequest(config, wireTransaction, options);
|
|
41
|
+
const res = await fetchImpl(req.url, {
|
|
42
|
+
method: req.method,
|
|
43
|
+
headers: req.headers,
|
|
44
|
+
body: req.body,
|
|
45
|
+
});
|
|
46
|
+
const body = (await res.json().catch(() => ({})));
|
|
47
|
+
if (body.error) {
|
|
48
|
+
throw new Error(`landing service rejected the transaction (HTTP ${res.status}): ${JSON.stringify(body.error)}`);
|
|
49
|
+
}
|
|
50
|
+
if (!body.result) {
|
|
51
|
+
throw new Error(`unexpected landing service response (HTTP ${res.status}): ${JSON.stringify(body)}`);
|
|
52
|
+
}
|
|
53
|
+
return body.result;
|
|
54
|
+
}
|
|
55
|
+
async function submitAndConfirm(wireTransaction, readConnection, params, options) {
|
|
56
|
+
const signature = await sendTransaction(wireTransaction, options);
|
|
57
|
+
const timeoutMs = params.timeoutMs ?? 90_000;
|
|
58
|
+
const pollIntervalMs = params.pollIntervalMs ?? 1_000;
|
|
59
|
+
const deadline = Date.now() + timeoutMs;
|
|
60
|
+
for (;;) {
|
|
61
|
+
const { value } = await readConnection.getSignatureStatuses([signature]);
|
|
62
|
+
const status = value?.[0];
|
|
63
|
+
if (status &&
|
|
64
|
+
(status.confirmationStatus === "confirmed" || status.confirmationStatus === "finalized")) {
|
|
65
|
+
if (status.err) {
|
|
66
|
+
throw new Error(`transaction landed but FAILED on-chain: ${JSON.stringify(status.err)} (${signature})`);
|
|
67
|
+
}
|
|
68
|
+
return { signature, slot: status.slot };
|
|
69
|
+
}
|
|
70
|
+
const height = await readConnection.getBlockHeight("confirmed");
|
|
71
|
+
if (height > params.lastValidBlockHeight || Date.now() > deadline) {
|
|
72
|
+
throw new Error(`transaction expired without confirming (${signature})`);
|
|
73
|
+
}
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { sendTransaction, submitAndConfirm };
|
|
78
|
+
}
|
package/dist/pda.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export declare function toPk(address: Address): web3.PublicKey;
|
|
|
6
6
|
export declare function deriveAta(owner: Address, mint: Address, tokenProgram?: Address): web3.PublicKey;
|
|
7
7
|
/** Tenant wrapper config PDA (seeds: ["tenant_wrapper_config"]). */
|
|
8
8
|
export declare function deriveWrapperConfig(wrapperProgramId: Address): web3.PublicKey;
|
|
9
|
+
/**
|
|
10
|
+
* Per-order replay-marker PDA (seeds: ["order", payer, order_id LE bytes]).
|
|
11
|
+
* Its existence is the wrapper's exactly-once gate: a second swap with the
|
|
12
|
+
* same (payer, order_id) fails at account creation and reverts entirely.
|
|
13
|
+
*/
|
|
14
|
+
export declare function deriveOrderMarker(wrapperProgramId: Address, payer: Address, orderId: bigint): web3.PublicKey;
|
|
9
15
|
/** Router GlobalConfig PDA (seeds: ["config"]). */
|
|
10
16
|
export declare function deriveRouterGlobalConfig(routerProgramId: Address): web3.PublicKey;
|
|
11
17
|
/** Router TenantConfig PDA (seeds: ["tenant", wrapper_program]). */
|
package/dist/pda.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as web3 from "@solana/web3.js";
|
|
2
|
-
import { ASSOCIATED_TOKEN_PROGRAM_ID, SEED_PUMPFUN_BONDING_CURVE, SEED_PUMPFUN_BONDING_CURVE_V2, SEED_PUMPFUN_CREATOR_VAULT, SEED_PUMPFUN_EVENT_AUTHORITY, SEED_PUMPFUN_FEE_CONFIG, SEED_PUMPFUN_GLOBAL, SEED_PUMPFUN_GLOBAL_VOLUME, SEED_PUMPFUN_USER_VOLUME, SEED_ROUTER_GLOBAL_CONFIG, SEED_ROUTER_TENANT, SEED_WRAPPER_CONFIG, TOKEN_PROGRAM_ID, } from "./constants.js";
|
|
2
|
+
import { ASSOCIATED_TOKEN_PROGRAM_ID, SEED_ORDER_MARKER, SEED_PUMPFUN_BONDING_CURVE, SEED_PUMPFUN_BONDING_CURVE_V2, SEED_PUMPFUN_CREATOR_VAULT, SEED_PUMPFUN_EVENT_AUTHORITY, SEED_PUMPFUN_FEE_CONFIG, SEED_PUMPFUN_GLOBAL, SEED_PUMPFUN_GLOBAL_VOLUME, SEED_PUMPFUN_USER_VOLUME, SEED_ROUTER_GLOBAL_CONFIG, SEED_ROUTER_TENANT, SEED_WRAPPER_CONFIG, TOKEN_PROGRAM_ID, } from "./constants.js";
|
|
3
3
|
/** Normalize an Address (base58 string or PublicKey) to a PublicKey. */
|
|
4
4
|
export function toPk(address) {
|
|
5
5
|
return typeof address === "string" ? new web3.PublicKey(address) : address;
|
|
@@ -15,6 +15,23 @@ export function deriveAta(owner, mint, tokenProgram = TOKEN_PROGRAM_ID) {
|
|
|
15
15
|
export function deriveWrapperConfig(wrapperProgramId) {
|
|
16
16
|
return pda([SEED_WRAPPER_CONFIG], toPk(wrapperProgramId));
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Per-order replay-marker PDA (seeds: ["order", payer, order_id LE bytes]).
|
|
20
|
+
* Its existence is the wrapper's exactly-once gate: a second swap with the
|
|
21
|
+
* same (payer, order_id) fails at account creation and reverts entirely.
|
|
22
|
+
*/
|
|
23
|
+
export function deriveOrderMarker(wrapperProgramId, payer, orderId) {
|
|
24
|
+
if (orderId <= 0n || orderId >= 1n << 128n) {
|
|
25
|
+
throw new Error("orderId must be a non-zero u128");
|
|
26
|
+
}
|
|
27
|
+
const le = new Uint8Array(16);
|
|
28
|
+
let n = orderId;
|
|
29
|
+
for (let i = 0; i < 16; i++) {
|
|
30
|
+
le[i] = Number(n & 0xffn);
|
|
31
|
+
n >>= 8n;
|
|
32
|
+
}
|
|
33
|
+
return pda([SEED_ORDER_MARKER, toPk(payer).toBytes(), le], toPk(wrapperProgramId));
|
|
34
|
+
}
|
|
18
35
|
/** Router GlobalConfig PDA (seeds: ["config"]). */
|
|
19
36
|
export function deriveRouterGlobalConfig(routerProgramId) {
|
|
20
37
|
return pda([SEED_ROUTER_GLOBAL_CONFIG], toPk(routerProgramId));
|
package/dist/swap.js
CHANGED
|
@@ -6,7 +6,7 @@ const { BN, BorshInstructionCoder } = anchor;
|
|
|
6
6
|
import { ASSOCIATED_TOKEN_PROGRAM_ID, FEE_MINT_ALLOWLIST, INSTRUCTIONS_SYSVAR_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, WSOL_MINT, } from "./constants.js";
|
|
7
7
|
import { TENANT_WRAPPER_IDL } from "./idl/tenantWrapper.js";
|
|
8
8
|
import { ROUTER_IDL } from "./idl/router.js";
|
|
9
|
-
import { deriveAta, derivePumpfunUserVolumeAccumulator, deriveRouterGlobalConfig, deriveRouterTenantConfig, deriveWrapperConfig, toPk, } from "./pda.js";
|
|
9
|
+
import { deriveAta, deriveOrderMarker, derivePumpfunUserVolumeAccumulator, deriveRouterGlobalConfig, deriveRouterTenantConfig, deriveWrapperConfig, toPk, } from "./pda.js";
|
|
10
10
|
import { buildPumpfunRemainingAccounts } from "./venues/pumpfun.js";
|
|
11
11
|
// One coder per program for the package lifetime; layouts derive from the
|
|
12
12
|
// vendored IDLs. NOTE: the raw (non-camelCased) IDLs are used, so encode()
|
|
@@ -53,6 +53,15 @@ export function buildPumpfunSwapInstruction(params) {
|
|
|
53
53
|
throw new Error("amountIn must be > 0");
|
|
54
54
|
if (params.minReturn <= 0n)
|
|
55
55
|
throw new Error("minReturn must be > 0");
|
|
56
|
+
if (context.mode === "tenant" && params.orderId === undefined) {
|
|
57
|
+
// The wrapper's order-marker replay gate makes order_id part of the dedup
|
|
58
|
+
// key, and its contract requires SERVER-AUTHORITATIVE ids — a random
|
|
59
|
+
// client-side default would silently break exactly-once semantics.
|
|
60
|
+
throw new Error("tenant mode requires an explicit orderId (server-authoritative, non-zero u128)");
|
|
61
|
+
}
|
|
62
|
+
if (params.orderId !== undefined && params.orderId === 0n) {
|
|
63
|
+
throw new Error("orderId must be non-zero");
|
|
64
|
+
}
|
|
56
65
|
const routerProgramId = context.routerProgramId;
|
|
57
66
|
const zeroblockFeeVault = context.zeroblockFeeVault;
|
|
58
67
|
const pumpMint = toPk(market.mint);
|
|
@@ -76,12 +85,18 @@ export function buildPumpfunSwapInstruction(params) {
|
|
|
76
85
|
const orderId = params.orderId ?? randomOrderId();
|
|
77
86
|
const dexVariant = side === "buy" ? { PumpfunBuy: {} } : { PumpfunSell: {} };
|
|
78
87
|
const amountIn = bnFromBigint(params.amountIn, "amountIn", 64);
|
|
88
|
+
const minReturn = bnFromBigint(params.minReturn, "minReturn", 64);
|
|
79
89
|
const swapArgs = {
|
|
80
90
|
amount_in: amountIn,
|
|
81
|
-
min_return:
|
|
91
|
+
min_return: minReturn,
|
|
82
92
|
amounts: [amountIn],
|
|
83
93
|
routes: [[{ dexes: [dexVariant], weights: Buffer.from([100]) }]],
|
|
84
|
-
|
|
94
|
+
// H2 (fail-closed slippage): the router REQUIRES an explicit downside
|
|
95
|
+
// bound via options.slippage and rejects a zero effective bound; the bare
|
|
96
|
+
// min_return field alone is not trusted. MinOut carries our computed bound.
|
|
97
|
+
// NOTE (M1): for sells the fee is skimmed from the wSOL output, and the
|
|
98
|
+
// bound is enforced on the user's NET receipt — quote accordingly.
|
|
99
|
+
options: { slippage: { MinOut: [minReturn] } },
|
|
85
100
|
};
|
|
86
101
|
// Random pick among the non-zero buyback recipients — the program validates
|
|
87
102
|
// membership on-chain, and ops pre-load all eight into the shared ALT.
|
|
@@ -134,6 +149,13 @@ export function buildPumpfunSwapInstruction(params) {
|
|
|
134
149
|
fixedKeys = [
|
|
135
150
|
{ pubkey: user, isWritable: true, isSigner: true },
|
|
136
151
|
{ pubkey: deriveWrapperConfig(wrapperProgramId), isWritable: false, isSigner: false },
|
|
152
|
+
// Exactly-once replay gate: init'd by the wrapper; a duplicate
|
|
153
|
+
// (payer, order_id) fails at create_account and reverts the swap.
|
|
154
|
+
{
|
|
155
|
+
pubkey: deriveOrderMarker(wrapperProgramId, user, orderId),
|
|
156
|
+
isWritable: true,
|
|
157
|
+
isSigner: false,
|
|
158
|
+
},
|
|
137
159
|
{ pubkey: routerProgramId, isWritable: false, isSigner: false },
|
|
138
160
|
{ pubkey: sourceTokenAccount, isWritable: true, isSigner: false },
|
|
139
161
|
{ pubkey: destinationTokenAccount, isWritable: true, isSigner: false },
|
package/dist/transaction.js
CHANGED
|
@@ -4,7 +4,7 @@ import { toPk } from "./pda.js";
|
|
|
4
4
|
import { buildComputeBudgetInstructions } from "./computeBudget.js";
|
|
5
5
|
import { buildPumpfunSwapInstruction } from "./swap.js";
|
|
6
6
|
import { buildTipInstruction, maskTipAccountInTables } from "./tip.js";
|
|
7
|
-
import {
|
|
7
|
+
import { closeAccountInstruction, createAtaIdempotentInstruction, unwrapWsolInstruction, wrapSolInstructions, } from "./wsol.js";
|
|
8
8
|
/** Rehydrate a streamed lookup table into web3.js form (compile-time only fields defaulted). */
|
|
9
9
|
export function toLookupTableAccount(data) {
|
|
10
10
|
return new web3.AddressLookupTableAccount({
|
|
@@ -45,26 +45,59 @@ export function buildPumpfunSwapTransaction(params) {
|
|
|
45
45
|
if (params.computeBudget) {
|
|
46
46
|
instructions.push(...buildComputeBudgetInstructions(params.computeBudget));
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
// TENANT MODE: the wrapper program manages the entire native-SOL lifecycle
|
|
49
|
+
// on-chain (ATA creation, wrap sized with exact on-chain fee headroom,
|
|
50
|
+
// post-swap sweep, proceeds unwrap, emptied-ATA cleanup) — the client adds
|
|
51
|
+
// NO wSOL/ATA instructions. DIRECT MODE: the router does none of that, so
|
|
52
|
+
// the client composes it here.
|
|
53
|
+
const clientManagesWsol = params.context.mode === "direct";
|
|
54
|
+
if (clientManagesWsol) {
|
|
55
|
+
if (params.side === "buy") {
|
|
56
|
+
if (params.wrapSol ?? true) {
|
|
57
|
+
// The router's input-side fee transfer draws from this same account
|
|
58
|
+
// AFTER the swap consumes amountIn — wrap fee headroom on top (ceil),
|
|
59
|
+
// or the fee transfer fails with insufficient funds.
|
|
60
|
+
const feeBps = BigInt(params.totalFeeBps ?? 0);
|
|
61
|
+
const feeHeadroom = (params.amountIn * feeBps + 9999n) / 10000n;
|
|
62
|
+
instructions.push(...wrapSolInstructions({ user, lamports: params.amountIn + feeHeadroom }));
|
|
63
|
+
}
|
|
64
|
+
instructions.push(createAtaIdempotentInstruction({
|
|
65
|
+
payer: user,
|
|
66
|
+
owner: user,
|
|
67
|
+
mint: toPk(params.market.mint),
|
|
68
|
+
tokenProgram: toPk(params.market.mintTokenProgram),
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Selling into wSOL: the wSOL ATA may not exist between trades.
|
|
73
|
+
instructions.push(createAtaIdempotentInstruction({ payer: user, owner: user, mint: toPk("So11111111111111111111111111111111111111112") }));
|
|
53
74
|
}
|
|
54
|
-
instructions.push(createAtaIdempotentInstruction({
|
|
55
|
-
payer: user,
|
|
56
|
-
owner: user,
|
|
57
|
-
mint: toPk(params.market.mint),
|
|
58
|
-
tokenProgram: toPk(params.market.mintTokenProgram),
|
|
59
|
-
}));
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
// Selling into wSOL: the wSOL ATA may not exist between trades.
|
|
63
|
-
instructions.push(createAtaIdempotentInstruction({ payer: user, owner: user, mint: toPk("So11111111111111111111111111111111111111112") }));
|
|
64
75
|
}
|
|
65
76
|
instructions.push(swap.instruction);
|
|
66
|
-
if (
|
|
67
|
-
|
|
77
|
+
if (clientManagesWsol) {
|
|
78
|
+
if (params.side === "buy" && (params.wrapSol ?? true)) {
|
|
79
|
+
// Sweep the wSOL account after the swap: returns unused fee headroom and
|
|
80
|
+
// the ATA rent as native SOL (closing a native account pays out ALL its
|
|
81
|
+
// lamports — that IS the unwrap).
|
|
82
|
+
instructions.push(closeAccountInstruction({
|
|
83
|
+
account: swap.sourceTokenAccount,
|
|
84
|
+
destination: user,
|
|
85
|
+
owner: user,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
if (params.side === "sell" && params.closeTokenAccount) {
|
|
89
|
+
// Full-balance sell: reclaim the empty token ATA's rent for the user.
|
|
90
|
+
// CloseAccount requires balance == 0, so a partial sell with this flag
|
|
91
|
+
// reverts the entire transaction (deliberate fail-loud behavior).
|
|
92
|
+
instructions.push(closeAccountInstruction({
|
|
93
|
+
account: swap.sourceTokenAccount,
|
|
94
|
+
destination: user,
|
|
95
|
+
owner: user,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
if (params.side === "sell" && (params.unwrapWsol ?? true)) {
|
|
99
|
+
instructions.push(unwrapWsolInstruction(user));
|
|
100
|
+
}
|
|
68
101
|
}
|
|
69
102
|
let tables = (params.lookupTables ?? []).map(toLookupTableAccount);
|
|
70
103
|
if (params.tip) {
|
package/dist/types.d.ts
CHANGED
|
@@ -69,7 +69,13 @@ export interface BuildPumpfunSwapParams {
|
|
|
69
69
|
amountIn: bigint;
|
|
70
70
|
/** Minimum acceptable output (quote minus slippage), raw units. */
|
|
71
71
|
minReturn: bigint;
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Business order id (non-zero u128), echoed in SwapResultEvent.
|
|
74
|
+
* TENANT MODE: REQUIRED and server-authoritative — it keys the wrapper's
|
|
75
|
+
* per-(payer, order_id) replay marker, so all retries/variants of one
|
|
76
|
+
* business order MUST reuse the same id (mint it on your backend).
|
|
77
|
+
* DIRECT MODE: optional; random when omitted (no replay marker).
|
|
78
|
+
*/
|
|
73
79
|
orderId?: bigint;
|
|
74
80
|
/** A FINALIZED blockhash (gateway forwards to a different staked node). */
|
|
75
81
|
recentBlockhash: string;
|
|
@@ -79,12 +85,25 @@ export interface BuildPumpfunSwapParams {
|
|
|
79
85
|
tip?: TipParams | null;
|
|
80
86
|
computeBudget?: ComputeBudgetParams | null;
|
|
81
87
|
/**
|
|
82
|
-
* buy only: prepend create-wSOL-ATA (idempotent) + transfer +
|
|
83
|
-
* the user trades from native SOL
|
|
88
|
+
* direct buy only: prepend create-wSOL-ATA (idempotent) + transfer +
|
|
89
|
+
* syncNative so the user trades from native SOL. Default true.
|
|
84
90
|
*/
|
|
85
91
|
wrapSol?: boolean;
|
|
86
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* direct buy only: total commission bps (0Block fee) used to size the wSOL
|
|
94
|
+
* wrap — the router collects input-side fees FROM the source wSOL account
|
|
95
|
+
* AFTER the swap consumes amountIn. Excess is swept back by the post-swap
|
|
96
|
+
* close. Default 0 (no headroom).
|
|
97
|
+
*/
|
|
98
|
+
totalFeeBps?: number;
|
|
99
|
+
/** direct sell only: close the wSOL ATA so proceeds land as native SOL. Default true. */
|
|
87
100
|
unwrapWsol?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* direct sell only: close the (now empty) token ATA after the swap,
|
|
103
|
+
* refunding its rent to the user. ONLY set when selling the ENTIRE balance
|
|
104
|
+
* — any remainder reverts the whole transaction. Default false.
|
|
105
|
+
*/
|
|
106
|
+
closeTokenAccount?: boolean;
|
|
88
107
|
}
|
|
89
108
|
export interface BuildSwapResult {
|
|
90
109
|
/** The unsigned v0 transaction. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0block.io/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Client-side transaction builder for the 0Block DEX router. Builds unsigned v0 swap transactions entirely offline
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Client-side transaction builder for the 0Block DEX router. Builds unsigned v0 swap transactions entirely offline \u2014 tenant-wrapped or direct \u2014 with shared address lookup tables, gateway tips, and on-chain fee-event decoding for accounting.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"solana",
|
|
7
7
|
"dex",
|
|
@@ -44,4 +44,4 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"typescript": "^5.5.4"
|
|
46
46
|
}
|
|
47
|
-
}
|
|
47
|
+
}
|