@coffer_so/sdk 1.0.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/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/clients/AdminClient.d.ts +194 -0
- package/dist/clients/AdminClient.d.ts.map +1 -0
- package/dist/clients/AdminClient.js +482 -0
- package/dist/clients/AdminClient.js.map +1 -0
- package/dist/clients/CofferBackendClient.d.ts +877 -0
- package/dist/clients/CofferBackendClient.d.ts.map +1 -0
- package/dist/clients/CofferBackendClient.js +536 -0
- package/dist/clients/CofferBackendClient.js.map +1 -0
- package/dist/clients/CubeBackendClient.d.ts +877 -0
- package/dist/clients/CubeBackendClient.d.ts.map +1 -0
- package/dist/clients/CubeBackendClient.js +536 -0
- package/dist/clients/CubeBackendClient.js.map +1 -0
- package/dist/clients/CubicPoolClient.d.ts +120 -0
- package/dist/clients/CubicPoolClient.d.ts.map +1 -0
- package/dist/clients/CubicPoolClient.js +687 -0
- package/dist/clients/CubicPoolClient.js.map +1 -0
- package/dist/clients/PoolFactoryClient.d.ts +53 -0
- package/dist/clients/PoolFactoryClient.d.ts.map +1 -0
- package/dist/clients/PoolFactoryClient.js +91 -0
- package/dist/clients/PoolFactoryClient.js.map +1 -0
- package/dist/clients/RpcClient.d.ts +49 -0
- package/dist/clients/RpcClient.d.ts.map +1 -0
- package/dist/clients/RpcClient.js +135 -0
- package/dist/clients/RpcClient.js.map +1 -0
- package/dist/clients/SingleTokenDepositClient.d.ts +67 -0
- package/dist/clients/SingleTokenDepositClient.d.ts.map +1 -0
- package/dist/clients/SingleTokenDepositClient.js +105 -0
- package/dist/clients/SingleTokenDepositClient.js.map +1 -0
- package/dist/clients/contract-instructions.d.ts +24 -0
- package/dist/clients/contract-instructions.d.ts.map +1 -0
- package/dist/clients/contract-instructions.js +130 -0
- package/dist/clients/contract-instructions.js.map +1 -0
- package/dist/clients/index.d.ts +10 -0
- package/dist/clients/index.d.ts.map +1 -0
- package/dist/clients/index.js +26 -0
- package/dist/clients/index.js.map +1 -0
- package/dist/clients/quote-math.d.ts +15 -0
- package/dist/clients/quote-math.d.ts.map +1 -0
- package/dist/clients/quote-math.js +98 -0
- package/dist/clients/quote-math.js.map +1 -0
- package/dist/clients/tx-builders.d.ts +231 -0
- package/dist/clients/tx-builders.d.ts.map +1 -0
- package/dist/clients/tx-builders.js +861 -0
- package/dist/clients/tx-builders.js.map +1 -0
- package/dist/clients/versioned.d.ts +34 -0
- package/dist/clients/versioned.d.ts.map +1 -0
- package/dist/clients/versioned.js +55 -0
- package/dist/clients/versioned.js.map +1 -0
- package/dist/config/index.d.ts +65 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +75 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/networks.d.ts +12 -0
- package/dist/config/networks.d.ts.map +1 -0
- package/dist/config/networks.js +51 -0
- package/dist/config/networks.js.map +1 -0
- package/dist/config/tokens.d.ts +19 -0
- package/dist/config/tokens.d.ts.map +1 -0
- package/dist/config/tokens.js +43 -0
- package/dist/config/tokens.js.map +1 -0
- package/dist/idl/cubic_pool.json +4203 -0
- package/dist/idl/index.d.ts +1769 -0
- package/dist/idl/index.d.ts.map +1 -0
- package/dist/idl/index.js +18 -0
- package/dist/idl/index.js.map +1 -0
- package/dist/idl/protocol_admin.json +2615 -0
- package/dist/idl/single_token_liquidity.json +1121 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +206 -0
- package/dist/index.js.map +1 -0
- package/dist/math/cubicMath.d.ts +40 -0
- package/dist/math/cubicMath.d.ts.map +1 -0
- package/dist/math/cubicMath.js +82 -0
- package/dist/math/cubicMath.js.map +1 -0
- package/dist/math/fixedPoint.d.ts +19 -0
- package/dist/math/fixedPoint.d.ts.map +1 -0
- package/dist/math/fixedPoint.js +86 -0
- package/dist/math/fixedPoint.js.map +1 -0
- package/dist/math/index.d.ts +9 -0
- package/dist/math/index.d.ts.map +1 -0
- package/dist/math/index.js +30 -0
- package/dist/math/index.js.map +1 -0
- package/dist/math/logExp.d.ts +6 -0
- package/dist/math/logExp.d.ts.map +1 -0
- package/dist/math/logExp.js +68 -0
- package/dist/math/logExp.js.map +1 -0
- package/dist/math/lpLoss.d.ts +44 -0
- package/dist/math/lpLoss.d.ts.map +1 -0
- package/dist/math/lpLoss.js +103 -0
- package/dist/math/lpLoss.js.map +1 -0
- package/dist/math/maxSelloff.d.ts +164 -0
- package/dist/math/maxSelloff.d.ts.map +1 -0
- package/dist/math/maxSelloff.js +397 -0
- package/dist/math/maxSelloff.js.map +1 -0
- package/dist/math/singleToken.d.ts +54 -0
- package/dist/math/singleToken.d.ts.map +1 -0
- package/dist/math/singleToken.js +188 -0
- package/dist/math/singleToken.js.map +1 -0
- package/dist/math/slippage.d.ts +13 -0
- package/dist/math/slippage.d.ts.map +1 -0
- package/dist/math/slippage.js +44 -0
- package/dist/math/slippage.js.map +1 -0
- package/dist/math/surgeFee.d.ts +24 -0
- package/dist/math/surgeFee.d.ts.map +1 -0
- package/dist/math/surgeFee.js +85 -0
- package/dist/math/surgeFee.js.map +1 -0
- package/dist/math/weightedMath.d.ts +20 -0
- package/dist/math/weightedMath.d.ts.map +1 -0
- package/dist/math/weightedMath.js +66 -0
- package/dist/math/weightedMath.js.map +1 -0
- package/dist/parsers/borsh.d.ts +28 -0
- package/dist/parsers/borsh.d.ts.map +1 -0
- package/dist/parsers/borsh.js +104 -0
- package/dist/parsers/borsh.js.map +1 -0
- package/dist/parsers/contracts.d.ts +26 -0
- package/dist/parsers/contracts.d.ts.map +1 -0
- package/dist/parsers/contracts.js +62 -0
- package/dist/parsers/contracts.js.map +1 -0
- package/dist/parsers/events.d.ts +3 -0
- package/dist/parsers/events.d.ts.map +1 -0
- package/dist/parsers/events.js +281 -0
- package/dist/parsers/events.js.map +1 -0
- package/dist/parsers/index.d.ts +6 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +22 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/mintAccount.d.ts +24 -0
- package/dist/parsers/mintAccount.d.ts.map +1 -0
- package/dist/parsers/mintAccount.js +27 -0
- package/dist/parsers/mintAccount.js.map +1 -0
- package/dist/parsers/poolAccount.d.ts +141 -0
- package/dist/parsers/poolAccount.d.ts.map +1 -0
- package/dist/parsers/poolAccount.js +224 -0
- package/dist/parsers/poolAccount.js.map +1 -0
- package/dist/types/contracts.d.ts +1340 -0
- package/dist/types/contracts.d.ts.map +1 -0
- package/dist/types/contracts.js +3 -0
- package/dist/types/contracts.js.map +1 -0
- package/dist/types/events.d.ts +172 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +3 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +22 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/pool.d.ts +149 -0
- package/dist/types/pool.d.ts.map +1 -0
- package/dist/types/pool.js +3 -0
- package/dist/types/pool.js.map +1 -0
- package/dist/types/result.d.ts +24 -0
- package/dist/types/result.d.ts.map +1 -0
- package/dist/types/result.js +11 -0
- package/dist/types/result.js.map +1 -0
- package/dist/types/tx.d.ts +261 -0
- package/dist/types/tx.d.ts.map +1 -0
- package/dist/types/tx.js +3 -0
- package/dist/types/tx.js.map +1 -0
- package/dist/utils/errors.d.ts +44 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +192 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/extensions.d.ts +98 -0
- package/dist/utils/extensions.d.ts.map +1 -0
- package/dist/utils/extensions.js +187 -0
- package/dist/utils/extensions.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/pda.d.ts +8 -0
- package/dist/utils/pda.d.ts.map +1 -0
- package/dist/utils/pda.js +27 -0
- package/dist/utils/pda.js.map +1 -0
- package/dist/utils/retry.d.ts +22 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +62 -0
- package/dist/utils/retry.js.map +1 -0
- package/package.json +54 -0
- package/src/clients/AdminClient.ts +608 -0
- package/src/clients/CofferBackendClient.ts +1413 -0
- package/src/clients/CubicPoolClient.ts +719 -0
- package/src/clients/PoolFactoryClient.ts +110 -0
- package/src/clients/RpcClient.ts +195 -0
- package/src/clients/SingleTokenDepositClient.ts +143 -0
- package/src/clients/contract-instructions.ts +115 -0
- package/src/clients/index.ts +9 -0
- package/src/clients/quote-math.ts +81 -0
- package/src/clients/tx-builders.ts +1096 -0
- package/src/clients/versioned.ts +80 -0
- package/src/config/index.ts +111 -0
- package/src/config/networks.ts +71 -0
- package/src/config/tokens.ts +52 -0
- package/src/idl/cubic_pool.json +4203 -0
- package/src/idl/index.ts +13 -0
- package/src/idl/protocol_admin.json +2615 -0
- package/src/idl/single_token_liquidity.json +1121 -0
- package/src/index.ts +262 -0
- package/src/math/cubicMath.ts +92 -0
- package/src/math/fixedPoint.ts +71 -0
- package/src/math/index.ts +13 -0
- package/src/math/logExp.ts +61 -0
- package/src/math/lpLoss.ts +160 -0
- package/src/math/maxSelloff.ts +564 -0
- package/src/math/singleToken.ts +251 -0
- package/src/math/slippage.ts +38 -0
- package/src/math/surgeFee.ts +101 -0
- package/src/math/weightedMath.ts +64 -0
- package/src/parsers/borsh.ts +106 -0
- package/src/parsers/contracts.ts +64 -0
- package/src/parsers/events.ts +275 -0
- package/src/parsers/index.ts +5 -0
- package/src/parsers/mintAccount.ts +44 -0
- package/src/parsers/poolAccount.ts +372 -0
- package/src/types/contracts.ts +238 -0
- package/src/types/events.ts +198 -0
- package/src/types/index.ts +5 -0
- package/src/types/pool.ts +147 -0
- package/src/types/result.ts +51 -0
- package/src/types/tx.ts +278 -0
- package/src/utils/errors.ts +210 -0
- package/src/utils/extensions.ts +196 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/pda.ts +58 -0
- package/src/utils/retry.ts +85 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ComputeBudgetProgram, Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
import BN from "bn.js";
|
|
3
|
+
import { CofferConfig } from "../config";
|
|
4
|
+
import { SdkResult, err, ok } from "../types/result";
|
|
5
|
+
import { BuiltTx, DeployPoolParams } from "../types/tx";
|
|
6
|
+
import {
|
|
7
|
+
buildDeployPoolTx,
|
|
8
|
+
buildInitializeCubicPoolIx,
|
|
9
|
+
buildPoolInitializeConfigIx,
|
|
10
|
+
} from "./tx-builders";
|
|
11
|
+
import { derivePoolPda } from "../utils/pda";
|
|
12
|
+
|
|
13
|
+
export interface PoolFactoryClientParams {
|
|
14
|
+
config: CofferConfig;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface InitializeConfigParams {
|
|
18
|
+
payer: PublicKey;
|
|
19
|
+
defaultProtocolFeeRate: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds transactions for protocol governance / pool deployment. Consumed
|
|
24
|
+
* by the deploy-pool UI flow and by the backend's admin tools.
|
|
25
|
+
*/
|
|
26
|
+
export class PoolFactoryClient {
|
|
27
|
+
readonly config: CofferConfig;
|
|
28
|
+
|
|
29
|
+
constructor(params: PoolFactoryClientParams) {
|
|
30
|
+
this.config = params.config;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Deploy a new pool atop an existing `CubicPoolConfig`. Caller is
|
|
35
|
+
* responsible for funding `params.payer` with enough SOL to cover rent
|
|
36
|
+
* for the pool account and the BPT mint PDA.
|
|
37
|
+
*
|
|
38
|
+
* Returns the `pool` and `bptMint` pubkeys it WILL create (derived PDAs)
|
|
39
|
+
* plus the signable tx.
|
|
40
|
+
*/
|
|
41
|
+
buildDeployPoolTx(params: DeployPoolParams): SdkResult<BuiltTx & { pool: PublicKey; bptMint: PublicKey }> {
|
|
42
|
+
if (params.tokens.length < 2 || params.tokens.length > 10) {
|
|
43
|
+
return err("invalid_input", "tokens.length must be in [2, 10]");
|
|
44
|
+
}
|
|
45
|
+
if (params.tokens.length !== params.weightsBps.length) {
|
|
46
|
+
return err("invalid_input", "tokens/weightsBps length mismatch");
|
|
47
|
+
}
|
|
48
|
+
if (params.tokens.length !== params.virtualBalances.length) {
|
|
49
|
+
return err("invalid_input", "tokens/virtualBalances length mismatch");
|
|
50
|
+
}
|
|
51
|
+
const sum = params.weightsBps.reduce((a, b) => a + b, 0);
|
|
52
|
+
if (sum !== 10_000) return err("invalid_input", `weights must sum to 10000 (got ${sum})`);
|
|
53
|
+
for (const vb of params.virtualBalances) {
|
|
54
|
+
if (vb.isZero()) return err("invalid_input", "virtualBalances must all be > 0");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const [pool] = derivePoolPda(this.config.programs.cubicPool, params.configKey, params.poolId);
|
|
58
|
+
const [bptMint] = PublicKey.findProgramAddressSync(
|
|
59
|
+
[Buffer.from("bpt_mint"), pool.toBuffer()],
|
|
60
|
+
this.config.programs.cubicPool
|
|
61
|
+
);
|
|
62
|
+
try {
|
|
63
|
+
const tx = buildDeployPoolTx(this.config, params);
|
|
64
|
+
return ok({ ...tx, pool, bptMint });
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return err("tx_build_failed", "Failed to build deploy-pool tx", e);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Initialize a new `CubicPoolConfig`, through the protocol-admin wrapper.
|
|
72
|
+
*
|
|
73
|
+
* Routed via `protocol_admin::pool_initialize_config` rather than
|
|
74
|
+
* `cubic_pool::initialize_config` directly: post-audit the latter requires
|
|
75
|
+
* the Treasury PDA to SIGN (H-01), and a PDA signature can only come from
|
|
76
|
+
* the owning program's `invoke_signed`. Building the direct instruction
|
|
77
|
+
* yields a transaction no wallet can ever satisfy.
|
|
78
|
+
*
|
|
79
|
+
* `params.payer` must be the current `Treasury.admin` — it is the human
|
|
80
|
+
* signer the wrapper authenticates. The returned `configKeypair` is the
|
|
81
|
+
* second required signer (the config account is `init`, not a PDA).
|
|
82
|
+
*/
|
|
83
|
+
buildInitializeConfigTx(params: InitializeConfigParams): SdkResult<
|
|
84
|
+
BuiltTx & { configKeypair: Keypair }
|
|
85
|
+
> {
|
|
86
|
+
try {
|
|
87
|
+
const configKeypair = Keypair.generate();
|
|
88
|
+
const ix = buildPoolInitializeConfigIx(this.config, {
|
|
89
|
+
config: configKeypair.publicKey,
|
|
90
|
+
admin: params.payer,
|
|
91
|
+
defaultProtocolFeeRate: params.defaultProtocolFeeRate,
|
|
92
|
+
});
|
|
93
|
+
return ok({
|
|
94
|
+
instructions: [ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), ix],
|
|
95
|
+
suggestedCuLimit: 200_000,
|
|
96
|
+
configKeypair,
|
|
97
|
+
});
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return err("tx_build_failed", "Failed to build initialize_config tx", e);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Low-level: return the raw `initialize_cubic_pool` ix without a CU
|
|
105
|
+
* budget or wrapping. Useful for combining with other ixs in the same tx.
|
|
106
|
+
*/
|
|
107
|
+
initializeCubicPoolIx(params: DeployPoolParams): TransactionInstruction {
|
|
108
|
+
return buildInitializeCubicPoolIx(this.config, params);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Commitment, Connection, PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { SdkResult } from "../types/result";
|
|
3
|
+
import { RetryOptions } from "../utils/retry";
|
|
4
|
+
import { err, ok } from "../types/result";
|
|
5
|
+
import { toSdkError } from "../utils/errors";
|
|
6
|
+
|
|
7
|
+
export interface RpcClientParams {
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
/** Ordered endpoint list. If provided, it is used as-is. */
|
|
10
|
+
endpoints?: string[];
|
|
11
|
+
/** Extra endpoints appended after `endpoint`. Ignored when `endpoints` is set. */
|
|
12
|
+
fallbackEndpoints?: string[];
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
commitment?: Commitment;
|
|
15
|
+
/** Optional headers (e.g. for Helius/Triton auth). */
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
/** Per-endpoint timeout before the next RPC endpoint is tried. */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
/** Backoff schedule between endpoint attempts. Defaults to no delay. */
|
|
20
|
+
backoffMs?: number[];
|
|
21
|
+
/** Test hook for injecting lightweight fake connections. */
|
|
22
|
+
connectionFactory?: (endpoint: string) => Pick<
|
|
23
|
+
Connection,
|
|
24
|
+
"getAccountInfo" | "getMultipleAccountsInfo" | "getSlot"
|
|
25
|
+
>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Thin wrapper around `@solana/web3.js` Connection with built-in retry +
|
|
30
|
+
* human-readable error mapping. Every public method returns a SdkResult<T>.
|
|
31
|
+
*/
|
|
32
|
+
export class RpcClient {
|
|
33
|
+
readonly endpoints: string[];
|
|
34
|
+
private readonly connections: Array<Pick<
|
|
35
|
+
Connection,
|
|
36
|
+
"getAccountInfo" | "getMultipleAccountsInfo" | "getSlot"
|
|
37
|
+
>>;
|
|
38
|
+
private readonly defaultCommitment: Commitment;
|
|
39
|
+
private readonly timeoutMs: number;
|
|
40
|
+
private readonly backoffMs: number[];
|
|
41
|
+
private activeEndpointIndex = 0;
|
|
42
|
+
|
|
43
|
+
constructor(params: RpcClientParams) {
|
|
44
|
+
this.defaultCommitment = params.commitment ?? "confirmed";
|
|
45
|
+
this.timeoutMs = params.timeoutMs ?? 2_000;
|
|
46
|
+
this.backoffMs = params.backoffMs ?? [];
|
|
47
|
+
this.endpoints = normalizeEndpoints(params);
|
|
48
|
+
if (this.endpoints.length === 0) {
|
|
49
|
+
throw new Error("RpcClient requires at least one endpoint");
|
|
50
|
+
}
|
|
51
|
+
this.connections = this.endpoints.map((endpoint) =>
|
|
52
|
+
params.connectionFactory
|
|
53
|
+
? params.connectionFactory(endpoint)
|
|
54
|
+
: new Connection(endpoint, {
|
|
55
|
+
commitment: this.defaultCommitment,
|
|
56
|
+
httpHeaders: params.headers,
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get connection(): Connection {
|
|
62
|
+
return this.connections[this.activeEndpointIndex] as Connection;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
call<T>(
|
|
66
|
+
fn: (conn: Connection) => Promise<T>,
|
|
67
|
+
retry: RetryOptions = {}
|
|
68
|
+
): Promise<SdkResult<T>> {
|
|
69
|
+
return this.callWithFallback((conn) => fn(conn as Connection), retry);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getAccountInfo(
|
|
73
|
+
pk: PublicKey,
|
|
74
|
+
retry: RetryOptions = {}
|
|
75
|
+
): Promise<SdkResult<{ data: Buffer; owner: PublicKey; lamports: number } | null>> {
|
|
76
|
+
return this.callWithFallback(async (conn) => {
|
|
77
|
+
const info = await conn.getAccountInfo(pk, this.defaultCommitment);
|
|
78
|
+
if (!info) return null;
|
|
79
|
+
return {
|
|
80
|
+
data: info.data,
|
|
81
|
+
owner: info.owner,
|
|
82
|
+
lamports: info.lamports,
|
|
83
|
+
};
|
|
84
|
+
}, retry);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getMultipleAccountsInfo(
|
|
88
|
+
pks: PublicKey[],
|
|
89
|
+
retry: RetryOptions = {}
|
|
90
|
+
): Promise<SdkResult<(Buffer | null)[]>> {
|
|
91
|
+
return this.callWithFallback(async (conn) => {
|
|
92
|
+
const infos = await conn.getMultipleAccountsInfo(pks, this.defaultCommitment);
|
|
93
|
+
return infos.map((i) => (i ? i.data : null));
|
|
94
|
+
}, retry);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getMultipleAccountsWithInfo(
|
|
98
|
+
pks: PublicKey[],
|
|
99
|
+
retry: RetryOptions = {}
|
|
100
|
+
): Promise<SdkResult<({ data: Buffer; owner: PublicKey; lamports: number } | null)[]>> {
|
|
101
|
+
return this.callWithFallback(async (conn) => {
|
|
102
|
+
const infos = await conn.getMultipleAccountsInfo(pks, this.defaultCommitment);
|
|
103
|
+
return infos.map((i) => i ? { data: i.data, owner: i.owner, lamports: i.lamports } : null);
|
|
104
|
+
}, retry);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getSlot(retry: RetryOptions = {}): Promise<SdkResult<number>> {
|
|
108
|
+
return this.callWithFallback((conn) => conn.getSlot(this.defaultCommitment), retry);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async callWithFallback<T>(
|
|
112
|
+
operation: (conn: Pick<Connection, "getAccountInfo" | "getMultipleAccountsInfo" | "getSlot">) => Promise<T>,
|
|
113
|
+
retry: RetryOptions = {}
|
|
114
|
+
): Promise<SdkResult<T>> {
|
|
115
|
+
const attempts = Math.max(1, retry.attempts ?? this.connections.length);
|
|
116
|
+
const timeoutMs = retry.timeoutMs ?? this.timeoutMs;
|
|
117
|
+
const shouldRetry = retry.shouldRetry ?? defaultShouldRetry;
|
|
118
|
+
let lastError: unknown;
|
|
119
|
+
|
|
120
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
121
|
+
const endpointIndex = (this.activeEndpointIndex + attempt) % this.connections.length;
|
|
122
|
+
try {
|
|
123
|
+
const data = await withTimeout(operation(this.connections[endpointIndex]), timeoutMs);
|
|
124
|
+
this.activeEndpointIndex = endpointIndex;
|
|
125
|
+
return ok(data);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
lastError = e;
|
|
128
|
+
if (!shouldRetry(e)) break;
|
|
129
|
+
if (attempt < attempts - 1) {
|
|
130
|
+
const delay = this.backoffMs[Math.min(attempt, this.backoffMs.length - 1)] ?? 0;
|
|
131
|
+
if (delay > 0) await sleep(delay);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sdkError = toSdkError(lastError);
|
|
137
|
+
return err(sdkError.code, sdkError.humanMessage, lastError);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeEndpoints(params: RpcClientParams): string[] {
|
|
142
|
+
if (params.endpoints && params.endpoints.length > 0) {
|
|
143
|
+
return dedupe(params.endpoints);
|
|
144
|
+
}
|
|
145
|
+
const primary = params.endpoint
|
|
146
|
+
? params.apiKey
|
|
147
|
+
? appendApiKey(params.endpoint, params.apiKey)
|
|
148
|
+
: params.endpoint
|
|
149
|
+
: undefined;
|
|
150
|
+
return dedupe([primary, ...(params.fallbackEndpoints ?? [])].filter(Boolean) as string[]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function dedupe(endpoints: string[]): string[] {
|
|
154
|
+
return Array.from(new Set(endpoints));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function defaultShouldRetry(e: unknown): boolean {
|
|
158
|
+
const sdkError = toSdkError(e);
|
|
159
|
+
return (
|
|
160
|
+
sdkError.code === "rpc_timeout" ||
|
|
161
|
+
sdkError.code === "rpc_unavailable" ||
|
|
162
|
+
sdkError.code === "rpc_rate_limited"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function sleep(ms: number): Promise<void> {
|
|
167
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
const h = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
|
|
173
|
+
p.then(
|
|
174
|
+
(v) => {
|
|
175
|
+
clearTimeout(h);
|
|
176
|
+
resolve(v);
|
|
177
|
+
},
|
|
178
|
+
(e) => {
|
|
179
|
+
clearTimeout(h);
|
|
180
|
+
reject(e);
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function appendApiKey(endpoint: string, apiKey: string): string {
|
|
187
|
+
try {
|
|
188
|
+
const u = new URL(endpoint);
|
|
189
|
+
u.searchParams.set("api-key", apiKey);
|
|
190
|
+
return u.toString();
|
|
191
|
+
} catch {
|
|
192
|
+
const sep = endpoint.includes("?") ? "&" : "?";
|
|
193
|
+
return `${endpoint}${sep}api-key=${encodeURIComponent(apiKey)}`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Commitment, PublicKey } from "@solana/web3.js";
|
|
2
|
+
import BN from "bn.js";
|
|
3
|
+
import { CofferConfig } from "../config";
|
|
4
|
+
import { PoolInfo } from "../types/pool";
|
|
5
|
+
import { SdkResult, err, ok } from "../types/result";
|
|
6
|
+
import {
|
|
7
|
+
BuiltTx,
|
|
8
|
+
SingleTokenDepositParams,
|
|
9
|
+
SingleTokenDepositQuote,
|
|
10
|
+
} from "../types/tx";
|
|
11
|
+
import { deriveHelperPda } from "../utils/pda";
|
|
12
|
+
import { buildSingleTokenDepositTx, buildSingleTokenDepositTxs } from "./tx-builders";
|
|
13
|
+
import { RpcClient } from "./RpcClient";
|
|
14
|
+
import { CubicPoolClient } from "./CubicPoolClient";
|
|
15
|
+
import { describeUnsupportedToken } from "../utils/extensions";
|
|
16
|
+
|
|
17
|
+
function unsupportedMessage(pool: PoolInfo, op: string): string {
|
|
18
|
+
return `${op} refused: ${(pool.unsupportedTokenIndices ?? []).filter(i => !pool.tokens[i].actualBalance.isZero()).map((i) => describeUnsupportedToken(pool.tokens[i])).join("; ")}. The cubic-pool program cannot transfer such tokens.`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SingleTokenDepositClientParams {
|
|
22
|
+
config: CofferConfig;
|
|
23
|
+
poolAddress: PublicKey;
|
|
24
|
+
/** Optional — reuses an existing one if provided. */
|
|
25
|
+
rpc?: RpcClient | {
|
|
26
|
+
endpoint?: string;
|
|
27
|
+
endpoints?: string[];
|
|
28
|
+
fallbackEndpoints?: string[];
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
commitment?: Commitment;
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Optional parent pool client — lets the deposit client reuse an already
|
|
35
|
+
* synced PoolInfo instead of making a fresh RPC round-trip.
|
|
36
|
+
*/
|
|
37
|
+
poolClient?: CubicPoolClient;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convenience class that wraps the single-token deposit flow end-to-end:
|
|
42
|
+
* quoting, transaction building, and helper-PDA derivation.
|
|
43
|
+
*
|
|
44
|
+
* Most consumers don't need to instantiate this directly — they can call
|
|
45
|
+
* `CubicPoolClient.singleTokenDeposit.*` which proxies here.
|
|
46
|
+
*/
|
|
47
|
+
export class SingleTokenDepositClient {
|
|
48
|
+
readonly config: CofferConfig;
|
|
49
|
+
readonly poolAddress: PublicKey;
|
|
50
|
+
private readonly poolClient: CubicPoolClient;
|
|
51
|
+
|
|
52
|
+
constructor(params: SingleTokenDepositClientParams) {
|
|
53
|
+
this.config = params.config;
|
|
54
|
+
this.poolAddress = params.poolAddress;
|
|
55
|
+
this.poolClient =
|
|
56
|
+
params.poolClient ??
|
|
57
|
+
new CubicPoolClient({
|
|
58
|
+
config: params.config,
|
|
59
|
+
poolAddress: params.poolAddress,
|
|
60
|
+
rpc:
|
|
61
|
+
params.rpc ??
|
|
62
|
+
{
|
|
63
|
+
endpoints: params.config.defaults.rpcEndpoints,
|
|
64
|
+
commitment: params.config.defaults.rpcCommitment,
|
|
65
|
+
timeoutMs: params.config.defaults.rpcTimeoutMs,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Derived helper PDA for this pool. */
|
|
71
|
+
helperPda(): PublicKey {
|
|
72
|
+
return deriveHelperPda(this.config.programs.singleTokenLiquidity, this.poolAddress)[0];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Ensure pool state is loaded; populates the underlying CubicPoolClient cache. */
|
|
76
|
+
async sync(): Promise<SdkResult<PoolInfo>> {
|
|
77
|
+
return this.poolClient.sync();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Off-chain estimate of the deposit outcome. */
|
|
81
|
+
quote(
|
|
82
|
+
tokenInIndex: number,
|
|
83
|
+
amountIn: BN,
|
|
84
|
+
slippageHundredthsBps?: number,
|
|
85
|
+
nowSeconds?: number,
|
|
86
|
+
helperBalances?: BN[]
|
|
87
|
+
): SdkResult<SingleTokenDepositQuote> {
|
|
88
|
+
return this.poolClient.quoteSingleTokenDeposit(tokenInIndex, amountIn, slippageHundredthsBps, nowSeconds, helperBalances);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build a ready-to-sign transaction. Includes idempotent helper-ATA
|
|
93
|
+
* creates.
|
|
94
|
+
*
|
|
95
|
+
* ⚠ Single-transaction form. It only fits for small pools — the ATA
|
|
96
|
+
* creates cost 5 CPI frames each and the zap itself uses 52 of the
|
|
97
|
+
* 64-frame budget at N=10. Use {@link buildTxs} for anything larger.
|
|
98
|
+
*/
|
|
99
|
+
buildTx(params: SingleTokenDepositParams): SdkResult<BuiltTx> {
|
|
100
|
+
const pool = this.poolClient.getCached();
|
|
101
|
+
if (!pool) return err("invalid_input", "Call sync() first to populate pool state");
|
|
102
|
+
if (!Number.isInteger(params.tokenInIndex) || params.tokenInIndex < 0 || params.tokenInIndex >= pool.tokenCount) {
|
|
103
|
+
return err("invalid_input", "Invalid tokenInIndex");
|
|
104
|
+
}
|
|
105
|
+
if (params.amountIn.lten(0)) return err("invalid_input", "amountIn must be > 0");
|
|
106
|
+
if ((pool.unsupportedTokenIndices ?? []).some(i => !pool.tokens[i].actualBalance.isZero())) {
|
|
107
|
+
return err("unsupported_token_extension", unsupportedMessage(pool, "deposit_single_token"));
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const tx = buildSingleTokenDepositTx(this.config, pool, params);
|
|
111
|
+
return ok(tx);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return err("tx_build_failed", "Failed to build single-token deposit tx", e);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Build the deposit as two transactions: `setup` (idempotent ATA creates)
|
|
119
|
+
* then `deposit`. Required for large pools; safe for all of them.
|
|
120
|
+
*
|
|
121
|
+
* Send `setup` and let it confirm before sending `deposit` — an ALT
|
|
122
|
+
* cannot be referenced in the same slot it was extended, and the two
|
|
123
|
+
* legs must not share an instruction-trace budget.
|
|
124
|
+
*/
|
|
125
|
+
buildTxs(
|
|
126
|
+
params: SingleTokenDepositParams
|
|
127
|
+
): SdkResult<{ setup: BuiltTx | null; deposit: BuiltTx }> {
|
|
128
|
+
const pool = this.poolClient.getCached();
|
|
129
|
+
if (!pool) return err("invalid_input", "Call sync() first to populate pool state");
|
|
130
|
+
if (!Number.isInteger(params.tokenInIndex) || params.tokenInIndex < 0 || params.tokenInIndex >= pool.tokenCount) {
|
|
131
|
+
return err("invalid_input", "Invalid tokenInIndex");
|
|
132
|
+
}
|
|
133
|
+
if (params.amountIn.lten(0)) return err("invalid_input", "amountIn must be > 0");
|
|
134
|
+
if ((pool.unsupportedTokenIndices ?? []).some(i => !pool.tokens[i].actualBalance.isZero())) {
|
|
135
|
+
return err("unsupported_token_extension", unsupportedMessage(pool, "deposit_single_token"));
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
return ok(buildSingleTokenDepositTxs(this.config, pool, params));
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return err("tx_build_failed", "Failed to build single-token deposit txs", e);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { BorshInstructionCoder, Idl } from "@coral-xyz/anchor";
|
|
2
|
+
import { AccountMeta, PublicKey, TransactionInstruction } from "@solana/web3.js";
|
|
3
|
+
import BN from "bn.js";
|
|
4
|
+
import { CofferConfig } from "../config";
|
|
5
|
+
import { IDLS } from "../idl";
|
|
6
|
+
import { ContractInstructionMap, ContractProgram } from "../types/contracts";
|
|
7
|
+
|
|
8
|
+
type InstructionName<P extends ContractProgram> = keyof ContractInstructionMap[P] & string;
|
|
9
|
+
export type ContractInstructionArgs<P extends ContractProgram, I extends InstructionName<P>> =
|
|
10
|
+
ContractInstructionMap[P][I] extends { args: infer A } ? A : never;
|
|
11
|
+
export type ContractInstructionAccounts<P extends ContractProgram, I extends InstructionName<P>> =
|
|
12
|
+
ContractInstructionMap[P][I] extends { accounts: infer A } ? A : never;
|
|
13
|
+
|
|
14
|
+
const coders = new Map<ContractProgram, BorshInstructionCoder>();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build any instruction exposed by the three deployed contracts. Argument and
|
|
18
|
+
* account names use the exact snake_case names from the shipped IDLs; all fixed
|
|
19
|
+
* accounts are explicit. Integer widths, argument fields, account order and
|
|
20
|
+
* signer/writable flags are checked or derived from those IDLs.
|
|
21
|
+
*
|
|
22
|
+
* This only builds an instruction. Required signatures, authority constraints,
|
|
23
|
+
* remaining-account layouts and transaction size still apply. A Treasury PDA
|
|
24
|
+
* can sign only during CPI: use the corresponding protocolAdmin wrapper for
|
|
25
|
+
* governance, rather than sending a cubicPool instruction with a PDA signer.
|
|
26
|
+
*/
|
|
27
|
+
export function buildContractInstruction<P extends ContractProgram, I extends InstructionName<P>>(
|
|
28
|
+
config: CofferConfig,
|
|
29
|
+
program: P,
|
|
30
|
+
instruction: I,
|
|
31
|
+
args: ContractInstructionArgs<P, I>,
|
|
32
|
+
accounts: ContractInstructionAccounts<P, I>,
|
|
33
|
+
remainingAccounts: AccountMeta[] = [],
|
|
34
|
+
): TransactionInstruction {
|
|
35
|
+
const idl = IDLS[program];
|
|
36
|
+
const schema = (idl.instructions as any[]).find((ix) => ix.name === instruction);
|
|
37
|
+
if (!schema) throw new Error(`Unknown ${program} instruction: ${instruction}`);
|
|
38
|
+
validateFields(args, schema.args, idl, `${program}.${instruction}`);
|
|
39
|
+
const supplied = accounts as Record<string, PublicKey>;
|
|
40
|
+
assertObjectFields(supplied, schema.accounts.map((a: any) => a.name), "accounts");
|
|
41
|
+
const keys: AccountMeta[] = schema.accounts.map((a: any) => ({
|
|
42
|
+
pubkey: publicKey(supplied[a.name], `accounts.${a.name}`),
|
|
43
|
+
isSigner: a.signer === true,
|
|
44
|
+
isWritable: a.writable === true,
|
|
45
|
+
}));
|
|
46
|
+
for (const meta of remainingAccounts) {
|
|
47
|
+
if (typeof meta.isSigner !== "boolean" || typeof meta.isWritable !== "boolean") {
|
|
48
|
+
throw new Error("remainingAccounts must contain explicit signer and writable flags");
|
|
49
|
+
}
|
|
50
|
+
keys.push({ ...meta, pubkey: publicKey(meta.pubkey, "remainingAccounts.pubkey") });
|
|
51
|
+
}
|
|
52
|
+
let coder = coders.get(program);
|
|
53
|
+
if (!coder) {
|
|
54
|
+
coder = new BorshInstructionCoder(idl as unknown as Idl);
|
|
55
|
+
coders.set(program, coder);
|
|
56
|
+
}
|
|
57
|
+
return new TransactionInstruction({
|
|
58
|
+
programId: config.programs[program],
|
|
59
|
+
keys,
|
|
60
|
+
data: coder.encode(instruction, args as any),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function publicKey(value: any, path: string): PublicKey {
|
|
65
|
+
if (!value || typeof value.toBuffer !== "function") throw new Error(`${path} must be a PublicKey`);
|
|
66
|
+
const bytes = value.toBuffer();
|
|
67
|
+
if (bytes.length !== 32) throw new Error(`${path} must be a 32-byte PublicKey`);
|
|
68
|
+
return new PublicKey(bytes);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function assertObjectFields(value: unknown, expected: string[], path: string): asserts value is Record<string, unknown> {
|
|
72
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must be an object`);
|
|
73
|
+
const names = Object.keys(value);
|
|
74
|
+
for (const key of expected) if (!Object.prototype.hasOwnProperty.call(value, key)) throw new Error(`${path}.${key} is required`);
|
|
75
|
+
for (const key of names) if (!expected.includes(key)) throw new Error(`${path}.${key} is not in the contract ABI`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validateFields(value: unknown, fields: any[], idl: any, path: string): void {
|
|
79
|
+
assertObjectFields(value, fields.map((f) => f.name), path);
|
|
80
|
+
for (const field of fields) validateType(value[field.name], field.type, idl, `${path}.${field.name}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateType(value: any, type: any, idl: any, path: string): void {
|
|
84
|
+
if (typeof type === "string") {
|
|
85
|
+
if (type === "pubkey") { publicKey(value, path); return; }
|
|
86
|
+
if (type === "bool") { if (typeof value !== "boolean") throw new Error(`${path} must be boolean`); return; }
|
|
87
|
+
if (type === "bytes") { if (!Buffer.isBuffer(value)) throw new Error(`${path} must be a Buffer`); return; }
|
|
88
|
+
const integer = /^([ui])(\d+)$/.exec(type);
|
|
89
|
+
if (integer) {
|
|
90
|
+
const width = Number(integer[2]);
|
|
91
|
+
const signed = integer[1] === "i";
|
|
92
|
+
if (width > 32 ? !BN.isBN(value) : !Number.isSafeInteger(value)) throw new Error(`${path} must be ${width > 32 ? "BN" : "an integer"} (${type})`);
|
|
93
|
+
const n = BN.isBN(value) ? value : new BN(value);
|
|
94
|
+
const min = signed ? new BN(1).shln(width - 1).neg() : new BN(0);
|
|
95
|
+
const max = new BN(1).shln(signed ? width - 1 : width).subn(1);
|
|
96
|
+
if (n.lt(min) || n.gt(max)) throw new Error(`${path} is outside ${type}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Unsupported ABI primitive ${type}`);
|
|
100
|
+
}
|
|
101
|
+
if (type.option) { if (value !== null) validateType(value, type.option, idl, path); return; }
|
|
102
|
+
if (type.vec || type.array) {
|
|
103
|
+
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
|
|
104
|
+
if (type.array && value.length !== type.array[1]) throw new Error(`${path} must have ${type.array[1]} entries`);
|
|
105
|
+
value.forEach((v, i) => validateType(v, type.vec ?? type.array[0], idl, `${path}[${i}]`));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (type.defined) {
|
|
109
|
+
const definition = idl.types.find((t: any) => t.name === type.defined.name);
|
|
110
|
+
if (!definition || definition.type.kind !== "struct") throw new Error(`Unsupported ABI type ${type.defined.name}`);
|
|
111
|
+
validateFields(value, definition.type.fields, idl, path);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Unsupported ABI type at ${path}`);
|
|
115
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./RpcClient";
|
|
2
|
+
export * from "./CofferBackendClient";
|
|
3
|
+
export * from "./CubicPoolClient";
|
|
4
|
+
export * from "./tx-builders";
|
|
5
|
+
export * from "./versioned";
|
|
6
|
+
export * from "./SingleTokenDepositClient";
|
|
7
|
+
export * from "./PoolFactoryClient";
|
|
8
|
+
export * from "./AdminClient";
|
|
9
|
+
export * from "./contract-instructions";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import BN from "bn.js";
|
|
2
|
+
import { PoolInfo, PoolTokenInfo } from "../types/pool";
|
|
3
|
+
import { calcOutGivenIn, calcSpotOut } from "../math/cubicMath";
|
|
4
|
+
import { calculateSwapFee, calculateProtocolFee } from "../math/slippage";
|
|
5
|
+
import { checkAndAdvanceSelloff } from "../math/maxSelloff";
|
|
6
|
+
import { calcSurgeFeeAmount } from "../math/surgeFee";
|
|
7
|
+
|
|
8
|
+
const U64_MAX = (1n << 64n) - 1n;
|
|
9
|
+
export function u64(value: bigint, label: string): bigint {
|
|
10
|
+
if (value < 0n || value > U64_MAX) throw new Error(`${label} must fit u64`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
const bi = (value: BN | undefined): bigint => BigInt(value?.toString() ?? "0");
|
|
14
|
+
|
|
15
|
+
/** Compute one deployed-contract swap against a snapshot, optionally advance a private zap copy. */
|
|
16
|
+
export function simulatePoolSwap(pool: PoolInfo, tokenInIndex: number, tokenOutIndex: number, amountIn: bigint, now: bigint, mutate = false) {
|
|
17
|
+
if (!Number.isInteger(tokenInIndex) || !Number.isInteger(tokenOutIndex) || tokenInIndex === tokenOutIndex ||
|
|
18
|
+
tokenInIndex < 0 || tokenOutIndex < 0 || tokenInIndex >= pool.tokenCount || tokenOutIndex >= pool.tokenCount) {
|
|
19
|
+
throw new Error("InvalidTokenIndex");
|
|
20
|
+
}
|
|
21
|
+
if (!pool.poolEnabled) throw new Error("PoolDisabled");
|
|
22
|
+
if (!pool.swapsEnabled) throw new Error("SwapsDisabled");
|
|
23
|
+
u64(amountIn, "amountIn");
|
|
24
|
+
if (amountIn === 0n) throw new Error("ZeroAmount");
|
|
25
|
+
const input = pool.tokens[tokenInIndex], output = pool.tokens[tokenOutIndex];
|
|
26
|
+
if (!input.isActive) throw new Error("TokenInactive");
|
|
27
|
+
const virtualBalanceIn = bi(input.virtualBalance), virtualBalanceOut = bi(output.virtualBalance);
|
|
28
|
+
const actualBalanceOut = bi(output.actualBalance);
|
|
29
|
+
if (input.maxSelloffPct > 0 && [input.maxSelloffPeriodLength, input.previousSelloff, input.currentSelloff,
|
|
30
|
+
input.windowStartTimestamp, input.selloffVbSnapshot].some(v => v === undefined)) {
|
|
31
|
+
throw new Error("Selloff state missing: sync the pool before quoting");
|
|
32
|
+
}
|
|
33
|
+
const window = checkAndAdvanceSelloff({
|
|
34
|
+
state: { previousSelloff: bi(input.previousSelloff), currentSelloff: bi(input.currentSelloff),
|
|
35
|
+
windowStartTimestamp: bi(input.windowStartTimestamp), selloffVbSnapshot: bi(input.selloffVbSnapshot) },
|
|
36
|
+
maxSelloffPct: input.maxSelloffPct, period: input.maxSelloffPeriodLength ?? 0,
|
|
37
|
+
amountIn, virtualBalance: virtualBalanceIn, now,
|
|
38
|
+
});
|
|
39
|
+
const feeAmount = calculateSwapFee(amountIn, pool.swapFeeRate);
|
|
40
|
+
const protocolFeeAmount = calculateProtocolFee(feeAmount, pool.protocolFeeRate);
|
|
41
|
+
const amountInAfterFee = amountIn - feeAmount;
|
|
42
|
+
const weightInBps = BigInt(input.weightBps), weightOutBps = BigInt(output.weightBps);
|
|
43
|
+
const grossAmountOut = calcOutGivenIn({ virtualBalanceIn, virtualBalanceOut, weightInBps, weightOutBps, amountIn: amountInAfterFee, actualBalanceOut });
|
|
44
|
+
const surgeFeeAmount = calcSurgeFeeAmount({ window,
|
|
45
|
+
thresholdPct: input.variableFeeThresholdPct ?? 0, slopeLowPct: input.variableFeeSlopeLowPct ?? 0,
|
|
46
|
+
slopeMidPct: input.variableFeeSlopeMidPct ?? 0, slopeHighPct: input.variableFeeSlopeHighPct ?? 0,
|
|
47
|
+
kinkPct: input.variableFeeKinkPct ?? 0, amountInAfterFee, amountOut: grossAmountOut,
|
|
48
|
+
virtualBalanceIn, virtualBalanceOut, weightInBps, weightOutBps, actualBalanceOut });
|
|
49
|
+
const amountOut = grossAmountOut - surgeFeeAmount;
|
|
50
|
+
const netInput = amountIn - protocolFeeAmount;
|
|
51
|
+
const actualInAfter = u64(bi(input.actualBalance) + netInput, "input actual balance");
|
|
52
|
+
const virtualInAfter = u64(virtualBalanceIn + netInput, "input virtual balance");
|
|
53
|
+
const actualOutAfter = u64(actualBalanceOut - grossAmountOut, "output actual balance");
|
|
54
|
+
const virtualOutAfter = u64(virtualBalanceOut - grossAmountOut, "output virtual balance");
|
|
55
|
+
const inputFees = u64(bi(input.protocolFeesOwed) + protocolFeeAmount, "input fees");
|
|
56
|
+
const outputFees = u64(bi(output.protocolFeesOwed) + surgeFeeAmount, "output fees");
|
|
57
|
+
if (mutate) {
|
|
58
|
+
input.actualBalance = new BN(actualInAfter.toString()); input.virtualBalance = new BN(virtualInAfter.toString());
|
|
59
|
+
output.actualBalance = new BN(actualOutAfter.toString()); output.virtualBalance = new BN(virtualOutAfter.toString());
|
|
60
|
+
input.protocolFeesOwed = new BN(inputFees.toString()); output.protocolFeesOwed = new BN(outputFees.toString());
|
|
61
|
+
if (window) {
|
|
62
|
+
input.previousSelloff = new BN(window.previousSelloff.toString()); input.currentSelloff = new BN(window.currentSelloff.toString());
|
|
63
|
+
input.windowStartTimestamp = new BN(window.windowStartTimestamp.toString()); input.selloffVbSnapshot = new BN(window.vbSnapshot.toString());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const spotOut = calcSpotOut({ virtualBalanceIn, virtualBalanceOut, weightInBps, weightOutBps, amountIn: amountInAfterFee });
|
|
67
|
+
// `window` is exposed for display math (post-trade fill %, span-average
|
|
68
|
+
// surge pct) — null when the input token has no sell-off cap configured.
|
|
69
|
+
return { amountOut, grossAmountOut, feeAmount, protocolFeeAmount, surgeFeeAmount, spotOut, window };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Clock from sync; callers may supply the transaction's expected Unix timestamp explicitly. */
|
|
73
|
+
export function quoteTimestamp(pool: PoolInfo, nowSeconds?: number): bigint {
|
|
74
|
+
const now = nowSeconds ?? pool.chainTimestamp;
|
|
75
|
+
if (now === undefined) {
|
|
76
|
+
if (pool.tokens.some(t => t.maxSelloffPct > 0)) throw new Error("Chain timestamp missing: sync or supply nowSeconds");
|
|
77
|
+
return 0n;
|
|
78
|
+
}
|
|
79
|
+
if (!Number.isSafeInteger(now) || now < 0) throw new Error("Invalid quote timestamp");
|
|
80
|
+
return BigInt(now);
|
|
81
|
+
}
|