@logswap/sdk 0.5.68 → 0.5.69

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/fpool.d.ts CHANGED
@@ -1273,6 +1273,27 @@ export declare function fPoolRelaunch(c: LogswapClient, a: {
1273
1273
  export declare const DEAD_ADDRESS: Address;
1274
1274
  /** The manager's `MIN_SHARES`: the seed mints this much to 0xdead on every pool, the rest to the seeder — or, on a pad, to 0xdead too. */
1275
1275
  export declare const F_MIN_SHARES = 1000n;
1276
+ /**
1277
+ * One genesis transaction: a swap the creator sends right after the seed, inside the launch's
1278
+ * `multicall`. A `buy` spends `amountIn` of quote for leg `j` (an index into `bases` AS GIVEN); a
1279
+ * `sell` spends `amountIn` of that leg for quote. The creator pays and is paid — the manager settles
1280
+ * to its sender — so a `recipient` is paid by an ERC-20 transfer of the row's output
1281
+ * ({@link fPoolGenesisTransfers}); without one the output stays with the creator.
1282
+ */
1283
+ export interface FPoolGenesisRow {
1284
+ side: "buy" | "sell";
1285
+ j: number;
1286
+ amountIn: bigint;
1287
+ /** Defaults to 0. With a `recipient`, {@link fPoolLaunch} pins it to the simulated output, which is what the transfer pays. */
1288
+ minOut?: bigint;
1289
+ recipient?: Address;
1290
+ }
1291
+ /** An ERC-20 transfer owed to a genesis recipient: one per (token, recipient), rows summed. */
1292
+ export interface FPoolGenesisTransfer {
1293
+ token: Address;
1294
+ to: Address;
1295
+ amount: bigint;
1296
+ }
1276
1297
  export interface FPoolLaunchArgs extends Omit<FPoolCreateArgs, "account"> {
1277
1298
  L0: bigint;
1278
1299
  /** Quote the seed pulls. A pad seeds at 0 — pure float, a resting ask, nothing raised. */
@@ -1289,6 +1310,18 @@ export interface FPoolLaunchArgs extends Omit<FPoolCreateArgs, "account"> {
1289
1310
  amountIn: bigint;
1290
1311
  minOut?: bigint;
1291
1312
  };
1313
+ /**
1314
+ * The genesis transactions: an ordered list of swaps right after the seed, each with an optional
1315
+ * recipient ({@link FPoolGenesisRow}). The same batch as `openBuy`, which stays as the one-buy
1316
+ * case and runs first when both are given. See `specs/f-pools/opening.md`.
1317
+ */
1318
+ openBatch?: FPoolGenesisRow[];
1319
+ /**
1320
+ * How a row's recipient is paid. `auto` (default): in the launch's own transaction when the
1321
+ * wallet batches atomically (EIP-5792), else left to {@link fPoolSendGenesisTransfers};
1322
+ * `followup`: always left to it. Ignored when no row names a recipient.
1323
+ */
1324
+ deliver?: "auto" | "followup";
1292
1325
  /** The pool's label (≤ 31 bytes of UTF-8) — the policy's word (049), written right after the batch by {@link fPoolLaunch}; not in the batch. */
1293
1326
  name?: string;
1294
1327
  /**
@@ -1312,6 +1345,32 @@ export declare function fPoolLaunchCalls(poolId: Hex, a: FPoolLaunchArgs & ({
1312
1345
  } | {
1313
1346
  x0: bigint[];
1314
1347
  })): Hex[];
1348
+ /** The genesis rows in the order they run: `openBuy` first when given, then `openBatch`. Legs as given. */
1349
+ export declare function fPoolGenesisRows(a: Pick<FPoolLaunchArgs, "openBuy" | "openBatch">): FPoolGenesisRow[];
1350
+ /**
1351
+ * What each genesis row returned, read by simulating the launch's `multicall` on the chain — the
1352
+ * contract's own rounding, no re-implementation. Exact at send time: the pool is new, and nothing
1353
+ * can trade between the seed and the rows. Needs the creator's funds and allowances, as the launch
1354
+ * does. One output per row, in {@link fPoolGenesisRows} order: base for a buy, quote for a sell.
1355
+ */
1356
+ export declare function fPoolGenesisOutputs(c: LogswapClient, poolId: Hex, a: FPoolLaunchArgs & ({
1357
+ r0: bigint[];
1358
+ } | {
1359
+ x0: bigint[];
1360
+ })): Promise<bigint[]>;
1361
+ /**
1362
+ * The transfers that pay the recipients: each row's output (`outs`, aligned with
1363
+ * {@link fPoolGenesisRows}) to its `recipient`, summed per (token, recipient), zero amounts dropped.
1364
+ * The token is the leg's base for a buy and the quote for a sell. Pure.
1365
+ */
1366
+ export declare function fPoolGenesisTransfers(a: Pick<FPoolLaunchArgs, "quote" | "bases" | "openBuy" | "openBatch">, outs: bigint[]): FPoolGenesisTransfer[];
1367
+ /** Pay genesis recipients with plain ERC-20 transfers, one transaction each, in order. For a launch that did not deliver itself. */
1368
+ export declare function fPoolSendGenesisTransfers(c: LogswapClient, a: {
1369
+ transfers: FPoolGenesisTransfer[];
1370
+ account: Address;
1371
+ }): Promise<Hash[]>;
1372
+ /** Whether the wallet runs a batch of calls atomically (EIP-5792 `atomic` capability), on the client's chain. A wallet without it, or one that errors, says no. */
1373
+ export declare function fPoolWalletBatchesAtomically(c: LogswapClient, account: Address): Promise<boolean>;
1315
1374
  /**
1316
1375
  * The desk's opening posture, on its policy: gated minting, the creator a
1317
1376
  * minter. No appointment: the authority is the operator until one is appointed.
@@ -1346,6 +1405,8 @@ export declare function fPoolLaunch(c: LogswapClient, a: FPoolLaunchArgs & ({
1346
1405
  })): Promise<{
1347
1406
  hash: Hash;
1348
1407
  poolId: Hex;
1408
+ transfers: FPoolGenesisTransfer[];
1409
+ delivered: boolean;
1349
1410
  }>;
1350
1411
  /**
1351
1412
  * Propose the next authority — step one of two, on the pool's policy. Nothing moves
package/dist/fpool.js CHANGED
@@ -29,7 +29,7 @@
29
29
  * twice, forever. One router is one spender — see `fPoolZapIn`/`fPoolZapOut` below, which target
30
30
  * `addresses.router`.
31
31
  */
32
- import { encodeAbiParameters, encodeFunctionData, keccak256, toFunctionSelector } from "viem";
32
+ import { decodeAbiParameters, encodeAbiParameters, encodeFunctionData, erc20Abi, keccak256, toFunctionSelector } from "viem";
33
33
  import { fPoolManagerAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
34
34
  /**
35
35
  * One contract, one ABI (F version 12): `harvest`, `claim`, `retire`, `regenerate`,
@@ -907,11 +907,11 @@ export function fPoolLaunchCalls(poolId, a) {
907
907
  calls.push(enc("seed", [poolId, a.L0, companions[1], a.Q0]));
908
908
  // gen 0 by construction: a launch is the pool's FIRST seed — a relaunch is `fPoolRelaunch`.
909
909
  // No transfer to 0xdead: on a pad `seed` mints every share there itself.
910
- if (a.openBuy) {
911
- const j = bases.indexOf(a.bases[a.openBuy.j]);
910
+ for (const row of fPoolGenesisRows(a)) {
911
+ const j = bases.indexOf(a.bases[row.j]);
912
912
  if (j < 0)
913
- throw new Error(`logswap: openBuy.j = ${a.openBuy.j} is not a leg`);
914
- calls.push(enc("swapQuoteIn", [poolId, BigInt(j), a.openBuy.amountIn, a.openBuy.minOut ?? 0n, a.account]));
913
+ throw new Error(`logswap: genesis row j = ${row.j} is not a leg`);
914
+ calls.push(enc(row.side === "buy" ? "swapQuoteIn" : "swapBaseIn", [poolId, BigInt(j), row.amountIn, row.minOut ?? 0n, a.account]));
915
915
  }
916
916
  // the name, the desk's gates and its list are the POLICY's, not the
917
917
  // manager's: they cannot ride in this batch — `fPoolLaunch` and `fPoolDeskOpening` send them
@@ -919,6 +919,80 @@ export function fPoolLaunchCalls(poolId, a) {
919
919
  calls.push(...(a.extra ?? []));
920
920
  return calls;
921
921
  }
922
+ /** The genesis rows in the order they run: `openBuy` first when given, then `openBatch`. Legs as given. */
923
+ export function fPoolGenesisRows(a) {
924
+ return [...(a.openBuy ? [{ side: "buy", ...a.openBuy }] : []), ...(a.openBatch ?? [])];
925
+ }
926
+ /**
927
+ * What each genesis row returned, read by simulating the launch's `multicall` on the chain — the
928
+ * contract's own rounding, no re-implementation. Exact at send time: the pool is new, and nothing
929
+ * can trade between the seed and the rows. Needs the creator's funds and allowances, as the launch
930
+ * does. One output per row, in {@link fPoolGenesisRows} order: base for a buy, quote for a sell.
931
+ */
932
+ export async function fPoolGenesisOutputs(c, poolId, a) {
933
+ const rows = fPoolGenesisRows(a);
934
+ if (rows.length === 0)
935
+ return [];
936
+ const calls = fPoolLaunchCalls(poolId, a);
937
+ const { result } = await c.public.simulateContract({
938
+ address: c.addresses.fPoolManager,
939
+ abi: fPoolManagerAbi,
940
+ functionName: "multicall",
941
+ args: [calls],
942
+ account: signerFor(c, a.account),
943
+ });
944
+ const results = result;
945
+ const first = (a.initialized ? 0 : 1) + 1; // [initialize] · seed · rows
946
+ return rows.map((_, i) => decodeAbiParameters([{ type: "uint256" }], results[first + i])[0]);
947
+ }
948
+ /**
949
+ * The transfers that pay the recipients: each row's output (`outs`, aligned with
950
+ * {@link fPoolGenesisRows}) to its `recipient`, summed per (token, recipient), zero amounts dropped.
951
+ * The token is the leg's base for a buy and the quote for a sell. Pure.
952
+ */
953
+ export function fPoolGenesisTransfers(a, outs) {
954
+ const out = new Map();
955
+ fPoolGenesisRows(a).forEach((row, i) => {
956
+ if (!row.recipient || !outs[i])
957
+ return;
958
+ const token = row.side === "buy" ? a.bases[row.j] : a.quote;
959
+ const k = `${token.toLowerCase()}:${row.recipient.toLowerCase()}`;
960
+ const t = out.get(k);
961
+ if (t)
962
+ t.amount += outs[i];
963
+ else
964
+ out.set(k, { token, to: row.recipient, amount: outs[i] });
965
+ });
966
+ return [...out.values()];
967
+ }
968
+ /** Pay genesis recipients with plain ERC-20 transfers, one transaction each, in order. For a launch that did not deliver itself. */
969
+ export async function fPoolSendGenesisTransfers(c, a) {
970
+ const wallet = requireWallet(c);
971
+ const hashes = [];
972
+ for (const t of a.transfers) {
973
+ const { request } = await c.public.simulateContract({
974
+ address: t.token,
975
+ abi: erc20Abi,
976
+ functionName: "transfer",
977
+ args: [t.to, t.amount],
978
+ account: signerFor(c, a.account),
979
+ });
980
+ hashes.push(await wallet.writeContract(request));
981
+ }
982
+ return hashes;
983
+ }
984
+ /** Whether the wallet runs a batch of calls atomically (EIP-5792 `atomic` capability), on the client's chain. A wallet without it, or one that errors, says no. */
985
+ export async function fPoolWalletBatchesAtomically(c, account) {
986
+ try {
987
+ const wallet = requireWallet(c);
988
+ const chainId = wallet.chain?.id;
989
+ const caps = (await wallet.getCapabilities({ account, chainId }));
990
+ return caps?.atomic?.status === "supported" || caps?.atomic?.status === "ready";
991
+ }
992
+ catch {
993
+ return false;
994
+ }
995
+ }
922
996
  /**
923
997
  * The desk's opening posture, on its policy: gated minting, the creator a
924
998
  * minter. No appointment: the authority is the operator until one is appointed.
@@ -951,11 +1025,42 @@ export async function fPoolDeskOpening(c, a) {
951
1025
  export async function fPoolLaunch(c, a) {
952
1026
  const poolId = await fPoolIdOf(c, a);
953
1027
  const policy = a.policy ?? fPoolPolicyFor(c, a.kind);
954
- const calls = fPoolLaunchCalls(poolId, { ...a, policy });
955
- const hash = await writeFPool0(c, "multicall", [calls], a.account);
1028
+ let args = { ...a, policy };
1029
+ // With recipients, each row's minOut is pinned to its simulated output and the transfers pay
1030
+ // exactly that: a row that returns less reverts the launch, a wei of surplus stays with the creator.
1031
+ let transfers = [];
1032
+ const rows = fPoolGenesisRows(a);
1033
+ if (rows.some((r) => r.recipient)) {
1034
+ const outs = await fPoolGenesisOutputs(c, poolId, args);
1035
+ args = { ...args, openBuy: undefined, openBatch: rows.map((r, i) => ({ ...r, minOut: outs[i] })) };
1036
+ transfers = fPoolGenesisTransfers(args, outs);
1037
+ }
1038
+ const calls = fPoolLaunchCalls(poolId, args);
1039
+ let hash;
1040
+ let delivered = transfers.length === 0;
1041
+ if (transfers.length > 0 && a.deliver !== "followup" && (await fPoolWalletBatchesAtomically(c, a.account))) {
1042
+ const wallet = requireWallet(c);
1043
+ const { id } = await wallet.sendCalls({
1044
+ account: signerFor(c, a.account),
1045
+ forceAtomic: true,
1046
+ calls: [
1047
+ { to: c.addresses.fPoolManager, data: encodeFunctionData({ abi: fPoolManagerAbi, functionName: "multicall", args: [calls] }) },
1048
+ ...transfers.map((t) => ({ to: t.token, data: encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [t.to, t.amount] }) })),
1049
+ ],
1050
+ });
1051
+ const status = await wallet.waitForCallsStatus({ id });
1052
+ const receipt = status.receipts?.[0];
1053
+ if (status.status !== "success" || !receipt)
1054
+ throw new Error("logswap: the launch batch did not succeed — nothing was created");
1055
+ hash = receipt.transactionHash;
1056
+ delivered = true;
1057
+ }
1058
+ else {
1059
+ hash = await writeFPool0(c, "multicall", [calls], a.account);
1060
+ }
956
1061
  if (a.name)
957
1062
  await fPoolSetName(c, { poolId, name: a.name, account: a.account, policy });
958
- return { hash, poolId };
1063
+ return { hash, poolId, transfers, delivered };
959
1064
  }
960
1065
  // ─── the roles, on the policy ─────────────────────────────────────────────
961
1066
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logswap/sdk",
3
- "version": "0.5.68",
3
+ "version": "0.5.69",
4
4
  "description": "TypeScript SDK for Logswap, the liquidity primitive introducing the logarithmic invariant — typed wrappers over the CPoolManager / FPoolManager / Router / Lens surface.",
5
5
  "license": "GPL-2.0-or-later",
6
6
  "type": "module",
package/src/fpool.ts CHANGED
@@ -30,7 +30,7 @@
30
30
  * `addresses.router`.
31
31
  */
32
32
 
33
- import { encodeAbiParameters, encodeFunctionData, keccak256, toFunctionSelector, type Address, type Hash, type Hex } from "viem";
33
+ import { decodeAbiParameters, encodeAbiParameters, encodeFunctionData, erc20Abi, keccak256, toFunctionSelector, type Address, type Hash, type Hex } from "viem";
34
34
  import { fPoolManagerAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
35
35
 
36
36
  /**
@@ -1252,6 +1252,29 @@ export const DEAD_ADDRESS: Address = "0x000000000000000000000000000000000000dEaD
1252
1252
  /** The manager's `MIN_SHARES`: the seed mints this much to 0xdead on every pool, the rest to the seeder — or, on a pad, to 0xdead too. */
1253
1253
  export const F_MIN_SHARES = 1000n;
1254
1254
 
1255
+ /**
1256
+ * One genesis transaction: a swap the creator sends right after the seed, inside the launch's
1257
+ * `multicall`. A `buy` spends `amountIn` of quote for leg `j` (an index into `bases` AS GIVEN); a
1258
+ * `sell` spends `amountIn` of that leg for quote. The creator pays and is paid — the manager settles
1259
+ * to its sender — so a `recipient` is paid by an ERC-20 transfer of the row's output
1260
+ * ({@link fPoolGenesisTransfers}); without one the output stays with the creator.
1261
+ */
1262
+ export interface FPoolGenesisRow {
1263
+ side: "buy" | "sell";
1264
+ j: number;
1265
+ amountIn: bigint;
1266
+ /** Defaults to 0. With a `recipient`, {@link fPoolLaunch} pins it to the simulated output, which is what the transfer pays. */
1267
+ minOut?: bigint;
1268
+ recipient?: Address;
1269
+ }
1270
+
1271
+ /** An ERC-20 transfer owed to a genesis recipient: one per (token, recipient), rows summed. */
1272
+ export interface FPoolGenesisTransfer {
1273
+ token: Address;
1274
+ to: Address;
1275
+ amount: bigint;
1276
+ }
1277
+
1255
1278
  export interface FPoolLaunchArgs extends Omit<FPoolCreateArgs, "account"> {
1256
1279
  L0: bigint;
1257
1280
  /** Quote the seed pulls. A pad seeds at 0 — pure float, a resting ask, nothing raised. */
@@ -1264,6 +1287,18 @@ export interface FPoolLaunchArgs extends Omit<FPoolCreateArgs, "account"> {
1264
1287
  * this buy — they are one transaction — so `minOut` is safe at its default of 0.
1265
1288
  */
1266
1289
  openBuy?: { j: number; amountIn: bigint; minOut?: bigint };
1290
+ /**
1291
+ * The genesis transactions: an ordered list of swaps right after the seed, each with an optional
1292
+ * recipient ({@link FPoolGenesisRow}). The same batch as `openBuy`, which stays as the one-buy
1293
+ * case and runs first when both are given. See `specs/f-pools/opening.md`.
1294
+ */
1295
+ openBatch?: FPoolGenesisRow[];
1296
+ /**
1297
+ * How a row's recipient is paid. `auto` (default): in the launch's own transaction when the
1298
+ * wallet batches atomically (EIP-5792), else left to {@link fPoolSendGenesisTransfers};
1299
+ * `followup`: always left to it. Ignored when no row names a recipient.
1300
+ */
1301
+ deliver?: "auto" | "followup";
1267
1302
  /** The pool's label (≤ 31 bytes of UTF-8) — the policy's word (049), written right after the batch by {@link fPoolLaunch}; not in the batch. */
1268
1303
  name?: string;
1269
1304
  /**
@@ -1301,10 +1336,10 @@ export function fPoolLaunchCalls(
1301
1336
  calls.push(enc("seed", [poolId, a.L0, companions[1]!, a.Q0]));
1302
1337
  // gen 0 by construction: a launch is the pool's FIRST seed — a relaunch is `fPoolRelaunch`.
1303
1338
  // No transfer to 0xdead: on a pad `seed` mints every share there itself.
1304
- if (a.openBuy) {
1305
- const j = bases.indexOf(a.bases[a.openBuy.j]!);
1306
- if (j < 0) throw new Error(`logswap: openBuy.j = ${a.openBuy.j} is not a leg`);
1307
- calls.push(enc("swapQuoteIn", [poolId, BigInt(j), a.openBuy.amountIn, a.openBuy.minOut ?? 0n, a.account]));
1339
+ for (const row of fPoolGenesisRows(a)) {
1340
+ const j = bases.indexOf(a.bases[row.j]!);
1341
+ if (j < 0) throw new Error(`logswap: genesis row j = ${row.j} is not a leg`);
1342
+ calls.push(enc(row.side === "buy" ? "swapQuoteIn" : "swapBaseIn", [poolId, BigInt(j), row.amountIn, row.minOut ?? 0n, a.account]));
1308
1343
  }
1309
1344
  // the name, the desk's gates and its list are the POLICY's, not the
1310
1345
  // manager's: they cannot ride in this batch — `fPoolLaunch` and `fPoolDeskOpening` send them
@@ -1313,6 +1348,87 @@ export function fPoolLaunchCalls(
1313
1348
  return calls;
1314
1349
  }
1315
1350
 
1351
+ /** The genesis rows in the order they run: `openBuy` first when given, then `openBatch`. Legs as given. */
1352
+ export function fPoolGenesisRows(a: Pick<FPoolLaunchArgs, "openBuy" | "openBatch">): FPoolGenesisRow[] {
1353
+ return [...(a.openBuy ? [{ side: "buy" as const, ...a.openBuy }] : []), ...(a.openBatch ?? [])];
1354
+ }
1355
+
1356
+ /**
1357
+ * What each genesis row returned, read by simulating the launch's `multicall` on the chain — the
1358
+ * contract's own rounding, no re-implementation. Exact at send time: the pool is new, and nothing
1359
+ * can trade between the seed and the rows. Needs the creator's funds and allowances, as the launch
1360
+ * does. One output per row, in {@link fPoolGenesisRows} order: base for a buy, quote for a sell.
1361
+ */
1362
+ export async function fPoolGenesisOutputs(
1363
+ c: LogswapClient,
1364
+ poolId: Hex,
1365
+ a: FPoolLaunchArgs & ({ r0: bigint[] } | { x0: bigint[] }),
1366
+ ): Promise<bigint[]> {
1367
+ const rows = fPoolGenesisRows(a);
1368
+ if (rows.length === 0) return [];
1369
+ const calls = fPoolLaunchCalls(poolId, a);
1370
+ const { result } = await c.public.simulateContract({
1371
+ address: c.addresses.fPoolManager!,
1372
+ abi: fPoolManagerAbi,
1373
+ functionName: "multicall",
1374
+ args: [calls],
1375
+ account: signerFor(c, a.account),
1376
+ } as never);
1377
+ const results = result as readonly Hex[];
1378
+ const first = (a.initialized ? 0 : 1) + 1; // [initialize] · seed · rows
1379
+ return rows.map((_, i) => decodeAbiParameters([{ type: "uint256" }], results[first + i]!)[0] as bigint);
1380
+ }
1381
+
1382
+ /**
1383
+ * The transfers that pay the recipients: each row's output (`outs`, aligned with
1384
+ * {@link fPoolGenesisRows}) to its `recipient`, summed per (token, recipient), zero amounts dropped.
1385
+ * The token is the leg's base for a buy and the quote for a sell. Pure.
1386
+ */
1387
+ export function fPoolGenesisTransfers(
1388
+ a: Pick<FPoolLaunchArgs, "quote" | "bases" | "openBuy" | "openBatch">,
1389
+ outs: bigint[],
1390
+ ): FPoolGenesisTransfer[] {
1391
+ const out = new Map<string, FPoolGenesisTransfer>();
1392
+ fPoolGenesisRows(a).forEach((row, i) => {
1393
+ if (!row.recipient || !outs[i]) return;
1394
+ const token = row.side === "buy" ? a.bases[row.j]! : a.quote;
1395
+ const k = `${token.toLowerCase()}:${row.recipient.toLowerCase()}`;
1396
+ const t = out.get(k);
1397
+ if (t) t.amount += outs[i]!;
1398
+ else out.set(k, { token, to: row.recipient, amount: outs[i]! });
1399
+ });
1400
+ return [...out.values()];
1401
+ }
1402
+
1403
+ /** Pay genesis recipients with plain ERC-20 transfers, one transaction each, in order. For a launch that did not deliver itself. */
1404
+ export async function fPoolSendGenesisTransfers(c: LogswapClient, a: { transfers: FPoolGenesisTransfer[]; account: Address }): Promise<Hash[]> {
1405
+ const wallet = requireWallet(c);
1406
+ const hashes: Hash[] = [];
1407
+ for (const t of a.transfers) {
1408
+ const { request } = await c.public.simulateContract({
1409
+ address: t.token,
1410
+ abi: erc20Abi,
1411
+ functionName: "transfer",
1412
+ args: [t.to, t.amount],
1413
+ account: signerFor(c, a.account),
1414
+ } as never);
1415
+ hashes.push(await wallet.writeContract(request as never));
1416
+ }
1417
+ return hashes;
1418
+ }
1419
+
1420
+ /** Whether the wallet runs a batch of calls atomically (EIP-5792 `atomic` capability), on the client's chain. A wallet without it, or one that errors, says no. */
1421
+ export async function fPoolWalletBatchesAtomically(c: LogswapClient, account: Address): Promise<boolean> {
1422
+ try {
1423
+ const wallet = requireWallet(c);
1424
+ const chainId = wallet.chain?.id;
1425
+ const caps = (await wallet.getCapabilities({ account, chainId })) as { atomic?: { status?: string } };
1426
+ return caps?.atomic?.status === "supported" || caps?.atomic?.status === "ready";
1427
+ } catch {
1428
+ return false;
1429
+ }
1430
+ }
1431
+
1316
1432
  /**
1317
1433
  * The desk's opening posture, on its policy: gated minting, the creator a
1318
1434
  * minter. No appointment: the authority is the operator until one is appointed.
@@ -1346,13 +1462,42 @@ export async function fPoolDeskOpening(c: LogswapClient, a: { poolId: Hex; accou
1346
1462
  export async function fPoolLaunch(
1347
1463
  c: LogswapClient,
1348
1464
  a: FPoolLaunchArgs & ({ r0: bigint[] } | { x0: bigint[] }),
1349
- ): Promise<{ hash: Hash; poolId: Hex }> {
1465
+ ): Promise<{ hash: Hash; poolId: Hex; transfers: FPoolGenesisTransfer[]; delivered: boolean }> {
1350
1466
  const poolId = await fPoolIdOf(c, a);
1351
1467
  const policy = a.policy ?? fPoolPolicyFor(c, a.kind);
1352
- const calls = fPoolLaunchCalls(poolId, { ...a, policy });
1353
- const hash = await writeFPool0(c, "multicall", [calls], a.account);
1468
+ let args = { ...a, policy };
1469
+ // With recipients, each row's minOut is pinned to its simulated output and the transfers pay
1470
+ // exactly that: a row that returns less reverts the launch, a wei of surplus stays with the creator.
1471
+ let transfers: FPoolGenesisTransfer[] = [];
1472
+ const rows = fPoolGenesisRows(a);
1473
+ if (rows.some((r) => r.recipient)) {
1474
+ const outs = await fPoolGenesisOutputs(c, poolId, args);
1475
+ args = { ...args, openBuy: undefined, openBatch: rows.map((r, i) => ({ ...r, minOut: outs[i]! })) };
1476
+ transfers = fPoolGenesisTransfers(args, outs);
1477
+ }
1478
+ const calls = fPoolLaunchCalls(poolId, args);
1479
+ let hash: Hash;
1480
+ let delivered = transfers.length === 0;
1481
+ if (transfers.length > 0 && a.deliver !== "followup" && (await fPoolWalletBatchesAtomically(c, a.account))) {
1482
+ const wallet = requireWallet(c);
1483
+ const { id } = await wallet.sendCalls({
1484
+ account: signerFor(c, a.account),
1485
+ forceAtomic: true,
1486
+ calls: [
1487
+ { to: c.addresses.fPoolManager!, data: encodeFunctionData({ abi: fPoolManagerAbi, functionName: "multicall", args: [calls] } as never) },
1488
+ ...transfers.map((t) => ({ to: t.token, data: encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [t.to, t.amount] }) })),
1489
+ ],
1490
+ } as never);
1491
+ const status = await wallet.waitForCallsStatus({ id });
1492
+ const receipt = status.receipts?.[0];
1493
+ if (status.status !== "success" || !receipt) throw new Error("logswap: the launch batch did not succeed — nothing was created");
1494
+ hash = receipt.transactionHash;
1495
+ delivered = true;
1496
+ } else {
1497
+ hash = await writeFPool0(c, "multicall", [calls], a.account);
1498
+ }
1354
1499
  if (a.name) await fPoolSetName(c, { poolId, name: a.name, account: a.account, policy });
1355
- return { hash, poolId };
1500
+ return { hash, poolId, transfers, delivered };
1356
1501
  }
1357
1502
 
1358
1503
  // ─── the roles, on the policy ─────────────────────────────────────────────