@0block.io/sdk 0.1.0 → 0.3.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/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: bnFromBigint(params.minReturn, "minReturn", 64),
91
+ min_return: minReturn,
82
92
  amounts: [amountIn],
83
93
  routes: [[{ dexes: [dexVariant], weights: Buffer.from([100]) }]],
84
- options: null,
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 },
@@ -17,3 +17,34 @@ export declare function buildPumpfunSwapTransaction(params: BuildPumpfunSwapPara
17
17
  * requiredSigners order and return the wire-ready bytes.
18
18
  */
19
19
  export declare function serializeWithSignatures(result: BuildSwapResult, signatures: Uint8Array[]): Uint8Array;
20
+ /**
21
+ * A submit template for a signer-and-submitter that is CHAIN-AGNOSTIC (e.g.
22
+ * sigcore): it signs `payload` and splices the raw signature into
23
+ * `wireTemplate` at `signatureOffset`, then broadcasts the result — with no
24
+ * knowledge of Solana's transaction format.
25
+ *
26
+ * The layout works because a Solana v0 transaction is
27
+ * `[compact-u16 sig count][sig0..sigN-1][message]`, and the bytes that get
28
+ * signed are exactly the `[message]` tail. `wireTemplate` is that full
29
+ * structure with the signature region zeroed; splicing the 64-byte signature
30
+ * over the zeros yields bytes identical to `serializeWithSignatures`.
31
+ *
32
+ * Single-signer only (the RAX flow): exactly one signature at a fixed offset.
33
+ */
34
+ export interface SubmitTemplate {
35
+ /** The bytes the signer must sign (the transaction message). Hex-encoded. */
36
+ payloadHex: string;
37
+ /** Full wire transaction with the signature zeroed. Base64-encoded. */
38
+ wireTemplateBase64: string;
39
+ /** Byte offset in the template where the 64-byte signature is spliced in. */
40
+ signatureOffset: number;
41
+ /** Signature length in bytes (64 for ed25519). */
42
+ signatureLength: number;
43
+ /** The single required signer's base58 address (the user). */
44
+ signer: string;
45
+ }
46
+ /**
47
+ * Emit a chain-agnostic {@link SubmitTemplate} from a built single-signer
48
+ * transaction. The offset is derived from the wire layout, not assumed.
49
+ */
50
+ export declare function buildSubmitTemplate(result: BuildSwapResult): SubmitTemplate;
@@ -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 { unwrapWsolInstruction, createAtaIdempotentInstruction, wrapSolInstructions } from "./wsol.js";
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
- if (params.side === "buy") {
49
- // Trade from native SOL (default): fund the wSOL ATA in-transaction, and
50
- // make sure the destination token ATA exists for first-time buyers.
51
- if (params.wrapSol ?? true) {
52
- instructions.push(...wrapSolInstructions({ user, lamports: params.amountIn }));
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 (params.side === "sell" && (params.unwrapWsol ?? true)) {
67
- instructions.push(unwrapWsolInstruction(user));
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) {
@@ -104,3 +137,55 @@ export function serializeWithSignatures(result, signatures) {
104
137
  }
105
138
  return result.transaction.serialize();
106
139
  }
140
+ /** compact-u16 (shortvec) encoded length — how Solana prefixes the sig array. */
141
+ function compactU16Length(n) {
142
+ if (n < 0x80)
143
+ return 1;
144
+ if (n < 0x4000)
145
+ return 2;
146
+ return 3;
147
+ }
148
+ /**
149
+ * Emit a chain-agnostic {@link SubmitTemplate} from a built single-signer
150
+ * transaction. The offset is derived from the wire layout, not assumed.
151
+ */
152
+ export function buildSubmitTemplate(result) {
153
+ const header = result.transaction.message.header;
154
+ if (header.numRequiredSignatures !== 1) {
155
+ throw new Error(`submit template supports a single signer; got ${header.numRequiredSignatures}`);
156
+ }
157
+ const SIG_LEN = 64;
158
+ // Zeroed-signature serialization IS the template (web3.js fills missing
159
+ // signatures with 64 zero bytes), so no manual zeroing is needed.
160
+ const wireTemplate = result.transaction.serialize();
161
+ const signatureOffset = compactU16Length(1); // count prefix precedes sig 0
162
+ // Invariant: the payload (message) is the tail after [count][signature].
163
+ const messageStart = signatureOffset + SIG_LEN;
164
+ const templateTail = wireTemplate.slice(messageStart);
165
+ if (templateTail.length !== result.messageBytes.length ||
166
+ !templateTail.every((b, i) => b === result.messageBytes[i])) {
167
+ throw new Error("submit template layout mismatch — message tail differs");
168
+ }
169
+ return {
170
+ payloadHex: bytesToHex(result.messageBytes),
171
+ wireTemplateBase64: toBase64(wireTemplate),
172
+ signatureOffset,
173
+ signatureLength: SIG_LEN,
174
+ signer: result.requiredSigners[0].toBase58(),
175
+ };
176
+ }
177
+ function bytesToHex(bytes) {
178
+ let hex = "";
179
+ for (const b of bytes)
180
+ hex += b.toString(16).padStart(2, "0");
181
+ return hex;
182
+ }
183
+ function toBase64(bytes) {
184
+ // Buffer in Node; btoa fallback in the browser.
185
+ if (typeof Buffer !== "undefined")
186
+ return Buffer.from(bytes).toString("base64");
187
+ let binary = "";
188
+ for (const b of bytes)
189
+ binary += String.fromCharCode(b);
190
+ return btoa(binary);
191
+ }
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
- /** Correlates the on-chain SwapResultEvent with an off-chain order. Random when omitted. */
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 + syncNative so
83
- * the user trades from native SOL without holding wSOL. Default true on buy.
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
- /** sell only: append close-wSOL-ATA so proceeds land as native SOL. Default true on sell. */
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.1.0",
4
- "description": "Client-side transaction builder for the 0Block DEX router. Builds unsigned v0 swap transactions entirely offline tenant-wrapped or direct with shared address lookup tables, gateway tips, and on-chain fee-event decoding for accounting.",
3
+ "version": "0.3.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
+ }