@tidepool-launchpad/sdk
v0.2.0
Published
TypeScript SDK for the Tidepool launchpad: launch tokens, trade the bonding curve, and follow the migration into Uniswap v4.
Downloads
440
Readme
@tidepool-launchpad/sdk
TypeScript SDK for the Tidepool launchpad. Launch tokens, trade the bonding curve, and follow the migration into a Uniswap v4 pool.
Built on viem. Works in Node and the browser.
pnpm add @tidepool-launchpad/sdk viemQuick start
import { createPublicClient, createWalletClient, http, parseEther } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { ink } from "viem/chains";
import { TidepoolClient } from "@tidepool-launchpad/sdk";
const publicClient = createPublicClient({ chain: ink, transport: http() });
const walletClient = createWalletClient({
chain: ink,
account: privateKeyToAccount(PRIVATE_KEY),
transport: http(),
});
const tidepool = new TidepoolClient({
factory: FACTORY, // the factory PROXY for your chain — see below
publicClient,
walletClient,
});Pass the chain when you build the clients — writes need it to assemble a
transaction, and the client checks both clients agree on it. Reads need no
wallet at all:
const readonly = new TidepoolClient({ factory: FACTORY, publicClient });Where FACTORY comes from. The launchpad is proxied and may be
redeployed, so the SDK does not bake addresses in. The current deployment for
each chain lives in deployments/<chainId>.json at the repo
root — read factory from the one matching your chain (57073 mainnet, 763373
Ink Sepolia), or from your own app config. Always the proxy, never the
implementation. For a local anvil stack, use the address your deploy script
prints.
Launching
const { token, curve } = await tidepool.launchToken({
name: "Example",
symbol: "EX",
uri: "ipfs://…",
targetRaise: parseEther("85"),
quoteToken: "0x0000000000000000000000000000000000000000", // native ETH
});A random salt is generated unless you supply one. It matters more than it
looks: the token address is CREATE2-derived from (sender, salt), and its pool
key follows. If a launch fails with EPoolAlreadyExists, someone initialized
that key first — retry with a different salt, not the same one.
launchTokenAndBuy launches and buys in a single transaction, so nobody can
buy between the two.
Trading
const quote = await tidepool.quoteBuy(curve, parseEther("1"));
// → { baseAmountOut, quoteAmountIn, feeAmount, refund, completesRaise }
await tidepool.buy(curve, parseEther("1")); // 1% slippage default
await tidepool.buy(curve, parseEther("1"), { slippageBps: 50 });
await tidepool.sell(curve, quote.baseAmountOut);buy and sell handle the parts that are easy to get wrong: they read whether
the curve is native- or ERC20-quoted, attach value or set an allowance
accordingly, quote first, and derive minAmountOut from your slippage
tolerance.
Two things worth knowing about quoteBuy:
quoteAmountIncan be less than what you offered. A buy that would overshoot the raise is capped at what the curve can still absorb, and the rest is refunded.refundtells you how much.completesRaisemeans this buy triggers migration. That transaction also creates and seeds the Uniswap v4 pool, so it costs considerably more gas. Budget for it, or the buy reverts.
Wallets the SDK cannot drive
Privy and other embedded or MPC signers never hand over a private key, so there
is no viem WalletClient to give the SDK. The prepare* methods build the
transactions and stop there — you broadcast them however your signer works.
const tidepool = new TidepoolClient({ factory, publicClient }); // no wallet
const txs = await tidepool.prepareBuy(curve, parseEther("1"), {
account: userAddress,
});
for (const tx of txs) {
await privyWallet.sendTransaction({ to: tx.to, data: tx.data, value: tx.value });
}prepareBuy and prepareSell return PreparedTransaction[].
Send them in order and wait for each to confirm. An ERC20 trade is two
transactions — approve, then trade — and the second reverts if the first has not
landed. A native-quote trade is a single entry with the ETH in value.
account is required: the allowance is read rather than assumed, so an approval
already covered by a standing allowance is left out instead of costing the user a
redundant signature. Approvals are for the exact amount, never unlimited.
These do not simulate, because the second step of a pair reads state the first has not written yet — simulating up front would fail on a trade that is going to succeed. Simulate each step after its predecessor confirms if you want a dry run.
Launches
prepareLaunchToken and prepareLaunchTokenAndBuy return
{ transactions, salt } instead. The SDK generates the salt when you do not
supply one, and it fixes the token's address — keep it, or you cannot reproduce
or correlate your own launch, and a retry past a squatted pool key needs a
different one.
const { transactions, salt } = await tidepool.prepareLaunchToken(args, {
account: userAddress,
});
let receipt;
for (const tx of transactions) {
const hash = await privyWallet.sendTransaction(tx);
receipt = await publicClient.waitForTransactionReceipt({ hash });
}
// You broadcast it, so read the result back yourself:
const { token, curve } = await tidepool.launchedFrom(receipt);launchedFrom accepts a receipt or a hash. Pass the receipt when you already
have one — it decodes the logs you are holding rather than making another round
trip.
Reading state
await tidepool.getConfig(); // fees, owner, pool manager, paused
await tidepool.getLaunchedTokens();
await tidepool.getCurveAddress(token);
await tidepool.getCurve(curve); // full state + progressBps
await tidepool.getPoolKey(token); // v4 pool identity
await tidepool.getMaxRaise(quoteToken); // cap for a quote asset
await tidepool.isCurve(address);getCurve returns progressBps (0–10000) measured by base sold, not quote
raised. The quote side moves non-linearly along the curve, so a quote-based
percentage badly misreports early progress.
Verifying a curve is real
if (!(await tidepool.isCurve(address))) throw new Error("not a Tidepool curve");Anyone can deploy a proxy off the public beacon and get genuine curve bytecode
that was never a real launch. Bytecode is not identity. Check isCurve
before showing a curve to users or indexing it.
After migration
Once migrated is true the curve is closed and the token trades on Uniswap v4.
poolSeeded confirms the pool was funded — the contracts make
migrated && !poolSeeded unreachable, so in practice they move together.
Note that after migration a curve's reserves describe history, not holdings:
the funds are in the pool. Don't render realQuoteReserves as a balance.
Errors
Reverts come back as TidepoolContractError with the contract's error name and
a plain-language reason where one exists — on reads and writes alike. A quote
or buy against a migrated curve raises ETokenAlreadyMigrated rather than
returning zeros, and a buy the account cannot fund fails with a message naming
the account and both amounts instead of a generic estimation error.
import { TidepoolContractError } from "@tidepool-launchpad/sdk";
try {
await tidepool.buy(curve, parseEther("1"));
} catch (e) {
if (e instanceof TidepoolContractError) {
console.error(e.errorName, e.message);
}
}Writes are simulated before sending, so a revert surfaces as a decoded error rather than a failed on-chain transaction.
Regenerating ABIs
forge build && pnpm gen:abisLicense
MIT. This covers the SDK only — the launchpad contracts in this repository are separately licensed.
