From d44b679f0f9f46e1bae6fc04f8c78a39e8a32b68 Mon Sep 17 00:00:00 2001
From: Nicholas Chikuji <61144399+chiku524@users.noreply.github.com>
Date: Thu, 5 Nov 2026 21:26:40 +0000
Subject: [PATCH 1/2] feat: one-shot create-and-seed pool create +
mint-at-deploy token fee
Wire Create Pool to the create-and-seed factory when published (one
Express approval). Credit Create Token platform fee in deploy init so
fee collection no longer needs a second approval.
Co-authored-by: nico.builds
---
docs/native-dex.md | 2 +-
frontend/.env.example | 3 +
frontend/scripts/boing-native-boing.test.mjs | 29 +++++++
.../components/NativeBoingCreatePoolPanel.jsx | 25 ++++++-
frontend/src/components/docs/docsContent.jsx | 10 +--
frontend/src/config/createAndSeedFactory.js | 35 +++++++++
frontend/src/services/boingLiquidityTx.js | 49 ++++++++++++
.../src/services/boingNativeCreatePool.js | 75 +++++++++++++++++--
.../src/services/boingNativeCreateToken.js | 22 +++++-
frontend/src/services/boingTokenV2Launch.js | 66 +++++++++++++---
10 files changed, 285 insertions(+), 31 deletions(-)
create mode 100644 frontend/src/config/createAndSeedFactory.js
diff --git a/docs/native-dex.md b/docs/native-dex.md
index ff5889a0..f4ddb11a 100644
--- a/docs/native-dex.md
+++ b/docs/native-dex.md
@@ -41,7 +41,7 @@ There is **no compiler switch** that turns Solidity into Boing VM bytecode. (The
- `nativeVm.dexFactory`, `swapRouter`, `ledgerRouterV2` / `V3`, `liquidityLocker`, plus LP vault / share token ids (env or live RPC; zeros when unpublished).
- Runtime defaults come from **`fetchNativeDexIntegrationDefaults`** (`@boing-network/boing-sdk`): live `boing_getNetworkInfo.end_user` plus env overrides. Historical SDK-embedded 6913 hexes are **not** treated as live.
-The **Swap** tab on 6913 targets the **network default** constant-product pool. **Create Pool** on 6913 deploys a new CP pool, then one Express batch binds token ids, seeds `add_liquidity`, and runs `register_pair` when a factory id is published (two user approvals — deploy cannot join a protocol batch). Amounts are human units (same as Swap). **Pools → Your liquidity** add/remove (Advanced section for a pasted id) works on the canonical pool, a directory venue, or a pasted pool AccountId. Additional factory pairs (including multihop) are surfaced under **Pools** and **Smart route**.
+The **Swap** tab on 6913 targets the **network default** constant-product pool. **Create Pool** on 6913 prefers the **create-and-seed** factory when `REACT_APP_BOING_NATIVE_CREATE_AND_SEED_FACTORY` is set: one Express Batch CREATE2s the pool, binds tokens, seeds `add_liquidity`, and `register_pair`s on the directory (one user approval). Without that factory, Create Pool falls back to deploy + one bind/seed/register batch (two approvals — deploy cannot join a protocol batch). Amounts are human units (same as Swap). **Pools → Your liquidity** add/remove (Advanced section for a pasted id) works on the canonical pool, a directory venue, or a pasted pool AccountId. Additional factory pairs (including multihop) are surfaced under **Pools** and **Smart route**.
### “Deploy the Solidity DEX on Boing” actually means
diff --git a/frontend/.env.example b/frontend/.env.example
index 89122e05..c4dd7985 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -56,6 +56,9 @@ REACT_APP_BOING_TESTNET_RPC_URL=https://testnet-rpc.boing.network/
# REACT_APP_BOING_NATIVE_AMM_POOL=0x…
# REACT_APP_BOING_NATIVE_VM_DEX_FACTORY=0x…
+# Create-and-seed factory (CREATE2 pool + bind + optional register). When set, Create Pool asks
+# for one Express approval. Deploy `native_dex_create_and_seed_bytecode` from boing.network first.
+# REACT_APP_BOING_NATIVE_CREATE_AND_SEED_FACTORY=0x…
# Linked NFT collection ↔ fungible token registry (32-byte AccountId). Required for on-chain
# claim/register/unlink on /linked-project; also accepts end_user.canonical_linked_nft_token_registry.
# Registry v2 (CREATE2 salt BOING_NFT_TOKEN_LINK_REG_V2, block 1174): an asset is claimed by its creator in its own
diff --git a/frontend/scripts/boing-native-boing.test.mjs b/frontend/scripts/boing-native-boing.test.mjs
index 63c7f2eb..b7e9cc9e 100644
--- a/frontend/scripts/boing-native-boing.test.mjs
+++ b/frontend/scripts/boing-native-boing.test.mjs
@@ -19,10 +19,12 @@ import {
} from '../src/services/boingSwapQuote.js';
import { poolCard, pairedAmount } from '../src/services/boingPoolsSimple.js';
import {
+ buildCreateAndSeedOneShotBatchRequest,
buildCreatePoolPostDeployBatchRequest,
buildNativeAddLiquidityRequest,
buildNativeRemoveLiquidityRequest,
} from '../src/services/boingLiquidityTx.js';
+import { boingPoolBytecodeHex } from '@boing-network/boing-sdk';
import { planNativeBoingSwap } from '../src/services/nativeDexCustodySwap.js';
const USDC = '0x6676f971ade3982406efa3e2cdb0477923f8f2a15624c4b6ad3a432817a75e93';
@@ -106,6 +108,33 @@ test('native add / remove liquidity are single batches (wrap with value, unwrap
assert.ok(rm.access_list.write.includes(WBOING_HEX));
});
+test('create-and-seed one-shot batch: wrap? + approves + create_and_seed + add_liquidity', () => {
+ const CAS = `0x${'c5'.repeat(32)}`;
+ const DIR = `0x${'cd'.repeat(32)}`;
+ const salt = `0x${'07'.repeat(32)}`;
+ const { poolHex, request } = buildCreateAndSeedOneShotBatchRequest({
+ factoryHex: CAS,
+ saltHex32: salt,
+ tokenAHex: WBOING_HEX,
+ tokenBHex: USDC,
+ amountA: E18,
+ amountB: 200n,
+ senderHex: ME,
+ directoryHex: DIR,
+ poolBytecodeHex: boingPoolBytecodeHex(8),
+ });
+ assert.equal(request.type, 'batch');
+ assert.match(poolHex, /^0x[0-9a-f]{64}$/);
+ // deposit + approve wBOING + approve USDC + create_and_seed + add_liquidity
+ assert.equal(request.calls.length, 5);
+ assert.equal(request.calls[0].contract, WBOING_HEX);
+ assert.equal(request.calls[0].value, String(E18));
+ assert.equal(request.calls[3].contract, CAS);
+ assert.equal(request.calls[4].contract, poolHex);
+ assert.ok(request.access_list.write.includes(CAS));
+ assert.ok(request.access_list.write.includes(DIR));
+});
+
test('create-pool post-deploy seed is one batch: set_tokens + wrap/approves/add + optional register', () => {
const FACTORY = `0x${'cd'.repeat(32)}`;
const withReg = buildCreatePoolPostDeployBatchRequest({
diff --git a/frontend/src/components/NativeBoingCreatePoolPanel.jsx b/frontend/src/components/NativeBoingCreatePoolPanel.jsx
index a4e21b88..f439420b 100644
--- a/frontend/src/components/NativeBoingCreatePoolPanel.jsx
+++ b/frontend/src/components/NativeBoingCreatePoolPanel.jsx
@@ -30,6 +30,7 @@ import { showDeployCelebration } from '../utils/deployCelebration';
import { buildBoingExplorerAccountUrl, buildBoingExplorerTxUrl } from '../config/boingExplorerUrls';
import { isBoingNativeAccountIdHex } from '../utils/boingWalletDiscovery';
import { normalizeNativeVmTokenId32 } from '../services/nativeVmTokenRegistry';
+import { isCreateAndSeedFactoryConfigured } from '../config/createAndSeedFactory';
const DEFAULT_POOL_PURPOSE = 'dapp';
@@ -317,7 +318,9 @@ export default function NativeBoingCreatePoolPanel() {
Create a pool
Pick two tokens and deposit the first liquidity. That first deposit sets the starting price. Boing Express will
- ask twice: deploy the pool, then one batch that binds tokens, seeds liquidity, and registers the pair.
+ {isCreateAndSeedFactoryConfigured()
+ ? ' ask once: create the pool, seed liquidity, and register the pair in a single batch.'
+ : ' ask twice: deploy the pool, then one batch that binds tokens, seeds liquidity, and registers the pair.'}
diff --git a/frontend/src/components/docs/docsContent.jsx b/frontend/src/components/docs/docsContent.jsx
index ed6ba4ce..0212599d 100644
--- a/frontend/src/components/docs/docsContent.jsx
+++ b/frontend/src/components/docs/docsContent.jsx
@@ -309,14 +309,14 @@ function Liquidity() {
Create pool starts a new pool: pick two tokens and make the first deposit, which sets
the starting price. Enter amounts the same way as Swap (human units — e.g. 200 BOING means 200 BOING,
- not 200 base units). Express confirms twice: deploy, then one batch that binds tokens, seeds liquidity, and
- registers the pair. The pool is registered on the DEX factory, so it shows on Pools and its tokens show in
- the swap list right away.
+ not 200 base units). When the create-and-seed factory is published, Express confirms once (create, seed, and
+ register in one batch); otherwise it asks twice (deploy, then bind/seed/register). Registered pools show on
+ Pools and their tokens show in the swap list right away.
Create Token deploys a Boing token v2 (whole units, fixed supply at create) —
- Standard one-token only. Platform fee is 0.5% of initial mint (same target as the bridge fee). After
- create, open Create pool with the new token to seed liquidity.
+ Standard one-token only. Platform fee is 0.5% of initial mint, credited to the treasury in the same
+ deploy approval. After create, open Create pool with the new token to seed liquidity.
>
diff --git a/frontend/src/config/createAndSeedFactory.js b/frontend/src/config/createAndSeedFactory.js
new file mode 100644
index 00000000..8c87ae8b
--- /dev/null
+++ b/frontend/src/config/createAndSeedFactory.js
@@ -0,0 +1,35 @@
+/**
+ * Create-and-seed factory AccountId (CREATE2 pool + bind + optional register in one call).
+ * When set, Create Pool uses a single Express Batch instead of deploy + seed.
+ * Deploy the factory from boing.network (`native_dex_create_and_seed_bytecode`) and publish here.
+ */
+
+function readEnv(name) {
+ try {
+ if (typeof process !== 'undefined' && process.env && typeof process.env[name] === 'string') {
+ return process.env[name].trim();
+ }
+ } catch {
+ /* ignore */
+ }
+ return '';
+}
+
+/** Normalize 32-byte AccountId hex (optional 0x) → lowercase `0x`+64 or ''. */
+export function normalizeCreateAndSeedFactoryHex(raw) {
+ const t = String(raw ?? '').trim();
+ if (!t) return '';
+ const hex = t.startsWith('0x') || t.startsWith('0X') ? t.slice(2) : t;
+ if (!/^[0-9a-fA-F]{64}$/.test(hex) || /^0+$/.test(hex)) return '';
+ return `0x${hex.toLowerCase()}`;
+}
+
+/** @returns {string} factory AccountId or '' when unset / invalid */
+export function getCreateAndSeedFactoryHex() {
+ return normalizeCreateAndSeedFactoryHex(readEnv('REACT_APP_BOING_NATIVE_CREATE_AND_SEED_FACTORY'));
+}
+
+/** True when Finance can offer one-approval create-and-seed. */
+export function isCreateAndSeedFactoryConfigured() {
+ return Boolean(getCreateAndSeedFactoryHex());
+}
diff --git a/frontend/src/services/boingLiquidityTx.js b/frontend/src/services/boingLiquidityTx.js
index 42b52aed..7d14f2a7 100644
--- a/frontend/src/services/boingLiquidityTx.js
+++ b/frontend/src/services/boingLiquidityTx.js
@@ -11,6 +11,10 @@
* set_tokens -> (wrap/approves/)add_liquidity -> optional factory register_pair
* so Express asks twice (deploy + seed), not four times. Deploy cannot join a batch: Express/protocol
* batch steps are transfer | contract_call only.
+ *
+ * When a create-and-seed factory is published (`REACT_APP_BOING_NATIVE_CREATE_AND_SEED_FACTORY`),
+ * Create Pool uses ONE batch instead: wrap? → approve → create_and_seed (CREATE2+bind+register) →
+ * add_liquidity — no top-level pool deploy.
*/
const lc = (h) => String(h || '').toLowerCase();
@@ -101,6 +105,48 @@ export function buildCreatePoolPostDeployBatchRequest(p) {
return boingTxToExpressRequest(tx);
}
+/**
+ * True one-approval Create Pool when the create-and-seed factory is published.
+ * @param {{
+ * factoryHex: string, saltHex32: string, tokenAHex: string, tokenBHex: string,
+ * amountA: bigint, amountB: bigint, senderHex: string, minLp?: bigint,
+ * directoryHex?: string | null, poolBytecodeHex?: string, wboingHex?: string,
+ * }} p
+ */
+export function buildCreateAndSeedOneShotBatchRequest(p) {
+ const tokenA = lc(p.tokenAHex);
+ const tokenB = lc(p.tokenBHex);
+ const w = lc(p.wboingHex || WBOING_HEX);
+ const poolBytecode = hexToBytes(p.poolBytecodeHex || boingPoolBytecodeHex(8));
+ let wrap = null;
+ let amountA = p.amountA;
+ let amountB = p.amountB;
+ let legA = tokenA;
+ let legB = tokenB;
+ if (isNativeBoingId(tokenA) || tokenA === w) {
+ wrap = { contractHex: w, calldata: encodeWboingDepositCalldata(), value: amountA };
+ legA = w;
+ } else if (isNativeBoingId(tokenB) || tokenB === w) {
+ wrap = { contractHex: w, calldata: encodeWboingDepositCalldata(), value: amountB };
+ legB = w;
+ }
+ const { poolHex, tx } = buildCreateAndSeedBatchTransaction({
+ senderHex: p.senderHex,
+ factoryHex: p.factoryHex,
+ saltHex32: p.saltHex32,
+ tokenAHex: legA,
+ tokenBHex: legB,
+ amountA,
+ amountB,
+ minLp: p.minLp ?? 1n,
+ directoryHex: p.directoryHex || null,
+ poolBytecode,
+ wrap,
+ nonce: 0n,
+ });
+ return { poolHex, request: boingTxToExpressRequest(tx) };
+}
+
/** Batch request for a native-BOING remove (exported for tests). */
export function buildNativeRemoveLiquidityRequest(p) {
const w = lc(p.wboingHex || WBOING_HEX);
@@ -123,10 +169,12 @@ export function buildNativeRemoveLiquidityRequest(p) {
const isNativePair = (p) => isNativeBoingId(p.tokenAHex) || isNativeBoingId(p.tokenBHex);
import {
boingContractCallToExpressTx,
+ boingPoolBytecodeHex,
boingTxToExpressRequest,
buildBatchTransaction,
buildBoingPoolCallTransaction,
buildBoingTokenApproveTransaction,
+ buildCreateAndSeedBatchTransaction,
encodeBoingPoolV8AddLiquidityCalldata,
encodeBoingPoolV8RemoveLiquidityCalldata,
encodeNativeAmmSetTokensCalldata,
@@ -135,6 +183,7 @@ import {
encodeWboingDepositCalldata,
encodeWboingWithdrawAllCalldata,
getBoingTokenAllowance,
+ hexToBytes,
} from '@boing-network/boing-sdk';
import { WBOING_HEX, isNativeBoingId } from './boingSwapQuote.js';
import { withBoingExpressDisplay } from './boingExpressTxDisplay.js';
diff --git a/frontend/src/services/boingNativeCreatePool.js b/frontend/src/services/boingNativeCreatePool.js
index 5656cf40..e1617c94 100644
--- a/frontend/src/services/boingNativeCreatePool.js
+++ b/frontend/src/services/boingNativeCreatePool.js
@@ -7,11 +7,20 @@ import { encodeNativeDexRegisterPairCalldataHex } from './nativeDexFactoryCallda
import { nativeAccountsAccessListJson } from './nativeAmmAccessList';
import { normalizeNativeVmTokenId32 } from './nativeVmTokenRegistry';
import { formatBoingExpressRpcError } from '../utils/boingExpressRpcError';
-import { boingPoolBytecodeHex, predictNonceDerivedContractAddress } from '@boing-network/boing-sdk';
-import { buildCreatePoolPostDeployBatchRequest, executeAddLiquidity } from './boingLiquidityTx';
+import {
+ boingPoolBytecodeHex,
+ predictNonceDerivedContractAddress,
+ randomCreateAndSeedPoolSaltHex,
+} from '@boing-network/boing-sdk';
+import {
+ buildCreateAndSeedOneShotBatchRequest,
+ buildCreatePoolPostDeployBatchRequest,
+ executeAddLiquidity,
+} from './boingLiquidityTx';
import { withBoingExpressDisplay } from './boingExpressTxDisplay';
import { getSharedBoingClient } from './boingNativeDexClient';
import { boingWalletMissingMessage } from '../utils/boingWalletDiscovery';
+import { getCreateAndSeedFactoryHex } from '../config/createAndSeedFactory';
const U64_MAX = (1n << 64n) - 1n;
@@ -236,6 +245,64 @@ export async function createBoingNativeConstantProductPool({
return { ok: false, code: 'no_provider', message: boingWalletMissingMessage() };
}
+ // No override -> custody pool v8: token-v2 pairs pulled with approvals, quoted and swapped by the swap card.
+ const custody = !String(customBytecode || '').trim();
+ const createAndSeedFactory = custody ? getCreateAndSeedFactoryHex() : '';
+ const factory = normalizeNativeVmTokenId32(factoryHex || '');
+
+ // True one-approval path: CREATE2 factory + Batch (approve → create_and_seed → add_liquidity).
+ if (createAndSeedFactory) {
+ try {
+ const saltHex32 = randomCreateAndSeedPoolSaltHex();
+ const { poolHex, request } = buildCreateAndSeedOneShotBatchRequest({
+ factoryHex: createAndSeedFactory,
+ saltHex32,
+ tokenAHex: tokenA,
+ tokenBHex: tokenB,
+ amountA: amountABn,
+ amountB: amountBBn,
+ minLp: 1n,
+ senderHex: account,
+ directoryHex: factory || null,
+ poolBytecodeHex: boingPoolBytecodeHex(8),
+ });
+ const batchTx = withBoingExpressDisplay(
+ request,
+ factory
+ ? 'Create pool, seed liquidity, and register the pair'
+ : 'Create pool and seed liquidity'
+ );
+ const oneShotHash = await boingExpressContractCallSignSimulateSubmit(provider, batchTx, {
+ senderHex: account,
+ waitForInclusion: true,
+ });
+ if (!factory) {
+ warnings.push(
+ 'No native DEX directory id is published on this network, so the pair was not registered for discovery.'
+ );
+ }
+ return {
+ ok: true,
+ poolId: poolHex,
+ deployTxHash: oneShotHash,
+ boingTxIdHex: oneShotHash,
+ setTokensTxHash: oneShotHash,
+ addLiquidityTxHash: oneShotHash,
+ registerTxHash: factory ? oneShotHash : null,
+ setTokensSkipped: false,
+ registerSkipped: !factory,
+ postDeployBatched: true,
+ createAndSeedOneShot: true,
+ custody: true,
+ warnings,
+ };
+ } catch (e) {
+ warnings.push(
+ `Create-and-seed one-shot failed (${formatBoingExpressRpcError(e)}); falling back to deploy + bind/seed.`
+ );
+ }
+ }
+
// Pool templates emit no deploy log, so the receipt alone does not name the new account: predict the
// nonce-derived address up front and confirm it has code once the deploy is included.
let predictedPoolId = null;
@@ -245,8 +312,6 @@ export async function createBoingNativeConstantProductPool({
predictedPoolId = null;
}
- // No override -> custody pool v8: token-v2 pairs pulled with approvals, quoted and swapped by the swap card.
- const custody = !String(customBytecode || '').trim();
const deploy = await executeBoingLaunchWizardDeploy({
kind: 'liquidity_pool',
getWalletProvider,
@@ -279,8 +344,6 @@ export async function createBoingNativeConstantProductPool({
};
}
- const factory = normalizeNativeVmTokenId32(factoryHex || '');
-
// Custody: one Express approval for bind + seed (+ register). Custom bytecode keeps the sequential path.
if (custody) {
try {
diff --git a/frontend/src/services/boingNativeCreateToken.js b/frontend/src/services/boingNativeCreateToken.js
index cddd8b10..582ef572 100644
--- a/frontend/src/services/boingNativeCreateToken.js
+++ b/frontend/src/services/boingNativeCreateToken.js
@@ -5,9 +5,9 @@
* marks mint_first used, and (when configured) claims the token in the linked NFT↔token registry.
*
* Platform fee (percent of initial mint) is planned in {@link planCreateTokenPlatformFee}.
- * When a fee recipient is configured and feeAmount > 0, a follow-up reference-token transfer
- * collects the fee share (separate Express approval). Live builds use the committed treasury
- * AccountId (overridable via `REACT_APP_CREATE_TOKEN_FEE_RECIPIENT`).
+ * When a fee recipient is configured and feeAmount > 0, the fee is credited to the treasury
+ * inside the mint-at-deploy init (same Express approval as create). Live builds use the committed
+ * treasury AccountId (overridable via `REACT_APP_CREATE_TOKEN_FEE_RECIPIENT`).
*/
import {
bytesToHex,
@@ -203,6 +203,10 @@ export async function createBoingNativeToken({
descriptionHashHex: input.descriptionHashHex,
runtimeHexOverride: input.bytecodeHexOverride,
claimRegistryHex,
+ // One-shot fee: mint treasury share in deploy init when collection is ready.
+ feeRecipientHex:
+ collectPlatformFee && platformFee.collectionReady ? platformFee.recipient : null,
+ feeAmount: collectPlatformFee && platformFee.collectionReady ? platformFee.feeAmount : 0n,
});
} catch (e) {
return { ok: false, code: 'prepare_failed', message: e?.message || 'Could not prepare the deploy.', platformFee };
@@ -278,7 +282,17 @@ export async function createBoingNativeToken({
});
let feeCollection;
- if (collectPlatformFee && platformFee.collectionReady) {
+ if (launch.feeMintedAtDeploy) {
+ feeCollection = {
+ ok: true,
+ skipped: false,
+ mintedAtDeploy: true,
+ feePlan: platformFee,
+ txHash,
+ boingTxIdHex,
+ };
+ } else if (collectPlatformFee && platformFee.collectionReady) {
+ // Legacy fallback if mint-at-deploy fee was not applied (e.g. custom runtime override).
feeCollection = await collectCreateTokenPlatformFee({
getWalletProvider,
account: sender,
diff --git a/frontend/src/services/boingTokenV2Launch.js b/frontend/src/services/boingTokenV2Launch.js
index 175b255d..ed24f923 100644
--- a/frontend/src/services/boingTokenV2Launch.js
+++ b/frontend/src/services/boingTokenV2Launch.js
@@ -136,16 +136,32 @@ export function buildBoingTokenV2LaunchTx(input) {
if (nonce < 0n) throw new Error('nonce must be >= 0');
const supply = parseBoingTokenV2Supply(input.supply);
const tokenHex = predictNonceDerivedContractAddress(senderHex, nonce).toLowerCase();
+ const feeRecipientHex = String(input.feeRecipientHex ?? '').trim() || null;
+ const feeAmount =
+ typeof input.feeAmount === 'bigint'
+ ? input.feeAmount
+ : input.feeAmount != null
+ ? BigInt(input.feeAmount)
+ : 0n;
const bytecode = buildBoingTokenV2MintAtDeployBytecodeHex({
supply,
runtimeHex: String(input.runtimeHexOverride ?? '').trim() || undefined,
claimRegistryHex: input.claimRegistryHex || null,
+ feeRecipientHex,
+ feeAmount,
});
const deployTx = {
...buildBoingTokenV2DeployMetaTx({ ...input, bytecodeHexOverride: bytecode }),
nonce: nonce.toString(),
};
- return { tokenHex, supply, deployTx };
+ return {
+ tokenHex,
+ supply,
+ deployTx,
+ feeMintedAtDeploy: Boolean(feeRecipientHex && feeAmount > 0n),
+ feeAmount,
+ feeRecipientHex: feeRecipientHex ? validateHex32(feeRecipientHex).toLowerCase() : null,
+ };
}
/* ------------------------------------------------------------------------------------------------
@@ -168,14 +184,33 @@ export const BOING_TOKEN_MINT_ONCE_SLOT_HEX = `0x${'00'.repeat(31)}f1`;
export { BOING_DEPLOY_INIT_MARKER };
/**
- * `0xFD || init` deploy bytecode: v2 runtime with the whole `supply` minted to the deployer at deploy.
- * With `claimRegistryHex` the init also calls the linked NFT↔token registry v2 `claim_self(deployer)`
- * (the registry faults, and so the whole deploy fails, if the token were somehow already claimed).
- * @param {{ supply: unknown, runtimeHex?: string, balanceXorMask?: Uint8Array, claimRegistryHex?: string | null }} input
+ * `0xFD || init` deploy bytecode: v2 runtime with supply minted at deploy.
+ * When `feeRecipientHex` + `feeAmount` are set, credits `supply - fee` to the deployer and `fee` to
+ * the treasury in the same init (one Express approval; no post-create transfer).
+ * With `claimRegistryHex` the init also calls the linked NFT↔token registry v2 `claim_self(deployer)`.
+ * @param {{ supply: unknown, runtimeHex?: string, balanceXorMask?: Uint8Array, claimRegistryHex?: string | null,
+ * feeRecipientHex?: string | null, feeAmount?: bigint }} input
* @returns {`0x${string}`}
*/
-export function buildBoingTokenV2MintAtDeployBytecodeHex({ supply, runtimeHex, balanceXorMask, claimRegistryHex = null } = {}) {
+export function buildBoingTokenV2MintAtDeployBytecodeHex({
+ supply,
+ runtimeHex,
+ balanceXorMask,
+ claimRegistryHex = null,
+ feeRecipientHex = null,
+ feeAmount = 0n,
+} = {}) {
const amount = parseBoingTokenV2Supply(supply);
+ let fee = typeof feeAmount === 'bigint' ? feeAmount : BigInt(feeAmount || 0);
+ if (fee < 0n) throw new Error('feeAmount must be >= 0');
+ if (fee > amount) throw new Error('feeAmount cannot exceed supply');
+ let feeTo = null;
+ if (fee > 0n) {
+ feeTo = validateHex32(feeRecipientHex || '').toLowerCase();
+ } else {
+ fee = 0n;
+ }
+ const creatorAmount = amount - fee;
const runtime = runtimeBytesForInit(runtimeHex || boingTokenV2BytecodeHex());
const mask = balanceXorMask || REFERENCE_FUNGIBLE_BALANCE_XOR;
if (!(mask instanceof Uint8Array) || mask.length !== 32) throw new Error('balance XOR mask must be 32 bytes');
@@ -184,11 +219,20 @@ export function buildBoingTokenV2MintAtDeployBytecodeHex({ supply, runtimeHex, b
w.op(OP.CALLER);
w.push32(hexToU8(BOING_TOKEN_ADMIN_SLOT_HEX));
w.op(OP.SSTORE);
- // balance[CALLER ^ mask] = supply
- w.push32(word(amount));
- w.op(OP.CALLER);
- w.push32(mask);
- w.op(OP.XOR, OP.SSTORE);
+ // balance[CALLER ^ mask] = creatorAmount (full supply when no fee)
+ if (creatorAmount > 0n) {
+ w.push32(word(creatorAmount));
+ w.op(OP.CALLER);
+ w.push32(mask);
+ w.op(OP.XOR, OP.SSTORE);
+ }
+ // balance[treasury ^ mask] = fee
+ if (fee > 0n && feeTo) {
+ w.push32(word(fee));
+ w.push32(hexToU8(feeTo));
+ w.push32(mask);
+ w.op(OP.XOR, OP.SSTORE);
+ }
// mint-once = 1
w.push32(word(1n));
w.push32(hexToU8(BOING_TOKEN_MINT_ONCE_SLOT_HEX));
From a0ab6aceeee4529355492dcc7310ef710398dbf9 Mon Sep 17 00:00:00 2001
From: Nicholas Chikuji <61144399+chiku524@users.noreply.github.com>
Date: Thu, 5 Nov 2026 21:38:01 +0000
Subject: [PATCH 2/2] fix(ux): align Create Token copy with mint-at-deploy fee
One Express approval credits the platform fee at deploy; stop promising
a separate fee transfer. Also honest Create Pool success when register
is skipped.
Co-authored-by: nico.builds
---
frontend/src/components/CreateTokenBoingContent.jsx | 10 ++++++----
frontend/src/components/NativeBoingCreatePoolPanel.jsx | 6 +++++-
frontend/src/config/createTokenFee.js | 2 +-
3 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/frontend/src/components/CreateTokenBoingContent.jsx b/frontend/src/components/CreateTokenBoingContent.jsx
index 9a9f0fa6..5f1374ce 100644
--- a/frontend/src/components/CreateTokenBoingContent.jsx
+++ b/frontend/src/components/CreateTokenBoingContent.jsx
@@ -272,8 +272,8 @@ export default function CreateTokenBoingContent() {
Boing token v2 — one Express signature creates
- the token and puts the whole supply in your account. Nobody can mint more afterwards. Add a pool next to
- make it swappable.
+ the token. Your wallet receives the mint minus the platform fee (fee goes to treasury at deploy). Nobody
+ can mint more afterwards. Add a pool next to make it swappable.
@@ -296,8 +296,8 @@ export default function CreateTokenBoingContent() {
Standard
- Create one fungible token. Express will ask you to approve the deploy
- {feePreview.collectionReady ? ', then a separate fee transfer' : ''}.
+ Create one fungible token. Express will ask once to approve the deploy
+ {feePreview.collectionReady ? ' (platform fee is credited to treasury in that same approval)' : ''}.