@tsunami-launchpad/sdk
v0.3.1
Published
TypeScript SDK for the Tsunami launchpad: launch tokens, trade the bonding curve, and follow the migration into Uniswap v4.
Maintainers
Readme
@tsunami-launchpad/sdk
TypeScript SDK for the Tsunami launchpad. Launch tokens, trade the bonding curve, and follow the migration into a Uniswap v4 pool.
Developer documentation: docs.tsunami.trade.
The previous package, @tidepool-launchpad/sdk, remains available. In version
0.3.0, TidepoolClient, TidepoolClientConfig, and TidepoolContractError
remain compatibility aliases for the new Tsunami exports. Existing imports
continue to work; switch packages and names when convenient. The error
name string remains TidepoolContractError for compatibility.
Built on viem. Works in Node and the browser.
pnpm add @tsunami-launchpad/sdk viemQuick start
import { createPublicClient, createWalletClient, http, parseEther } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { ink } from "viem/chains";
import { TsunamiClient } from "@tsunami-launchpad/sdk";
const publicClient = createPublicClient({ chain: ink, transport: http() });
const walletClient = createWalletClient({
chain: ink,
account: privateKeyToAccount(PRIVATE_KEY),
transport: http(),
});
const tsunami = new TsunamiClient({
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 TsunamiClient({ 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 is listed in the deployment address reference.
Choose the factory matching your chain (57073 mainnet, 763373 Ink Sepolia),
or read it 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 tsunami.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 tsunami.quoteBuy(curve, parseEther("1"));
// → { baseAmountOut, quoteAmountIn, feeAmount, refund, completesRaise }
await tsunami.buy(curve, parseEther("1")); // 1% slippage default
await tsunami.buy(curve, parseEther("1"), { slippageBps: 50 });
await tsunami.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 tsunami = new TsunamiClient({ factory, publicClient }); // no wallet
const txs = await tsunami.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 tsunami.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 tsunami.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 tsunami.getConfig(); // fees, owner, pool manager, paused
await tsunami.getLaunchedTokens();
await tsunami.getCurveAddress(token);
await tsunami.getCurve(curve); // full state + progressBps
await tsunami.getPoolKey(token); // v4 pool identity
await tsunami.getMaxRaise(quoteToken); // cap for a quote asset
await tsunami.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 tsunami.isCurve(address))) throw new Error("not a Tsunami 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 TsunamiContractError 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 { TsunamiContractError } from "@tsunami-launchpad/sdk";
try {
await tsunami.buy(curve, parseEther("1"));
} catch (e) {
if (e instanceof TsunamiContractError) {
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.
