@peddles/sdk 0.2.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/CLAUDE_PROMPT.md +159 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/abis.d.ts +19 -0
- package/dist/abis.d.ts.map +1 -0
- package/dist/abis.generated.d.ts +2029 -0
- package/dist/abis.generated.d.ts.map +1 -0
- package/dist/abis.generated.js +2723 -0
- package/dist/abis.generated.js.map +1 -0
- package/dist/abis.js +19 -0
- package/dist/abis.js.map +1 -0
- package/dist/artDex.d.ts +937 -0
- package/dist/artDex.d.ts.map +1 -0
- package/dist/artDex.js +155 -0
- package/dist/artDex.js.map +1 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -0
- package/dist/clog.d.ts +75 -0
- package/dist/clog.d.ts.map +1 -0
- package/dist/clog.js +91 -0
- package/dist/clog.js.map +1 -0
- package/dist/clogInflow.d.ts +136 -0
- package/dist/clogInflow.d.ts.map +1 -0
- package/dist/clogInflow.js +177 -0
- package/dist/clogInflow.js.map +1 -0
- package/dist/deployments.d.ts +30 -0
- package/dist/deployments.d.ts.map +1 -0
- package/dist/deployments.generated.d.ts +92 -0
- package/dist/deployments.generated.d.ts.map +1 -0
- package/dist/deployments.generated.js +93 -0
- package/dist/deployments.generated.js.map +1 -0
- package/dist/deployments.js +52 -0
- package/dist/deployments.js.map +1 -0
- package/dist/feeSplit.d.ts +54 -0
- package/dist/feeSplit.d.ts.map +1 -0
- package/dist/feeSplit.js +69 -0
- package/dist/feeSplit.js.map +1 -0
- package/dist/feeTerms.d.ts +46 -0
- package/dist/feeTerms.d.ts.map +1 -0
- package/dist/feeTerms.js +80 -0
- package/dist/feeTerms.js.map +1 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +108 -0
- package/dist/index.js.map +1 -0
- package/dist/launch/abi.generated.d.ts +2667 -0
- package/dist/launch/abi.generated.d.ts.map +1 -0
- package/dist/launch/abi.generated.js +2472 -0
- package/dist/launch/abi.generated.js.map +1 -0
- package/dist/launch/addresses.d.ts +24 -0
- package/dist/launch/addresses.d.ts.map +1 -0
- package/dist/launch/addresses.js +35 -0
- package/dist/launch/addresses.js.map +1 -0
- package/dist/launch/feeTerms.d.ts +52 -0
- package/dist/launch/feeTerms.d.ts.map +1 -0
- package/dist/launch/feeTerms.js +72 -0
- package/dist/launch/feeTerms.js.map +1 -0
- package/dist/launch/index.d.ts +33 -0
- package/dist/launch/index.d.ts.map +1 -0
- package/dist/launch/index.js +24 -0
- package/dist/launch/index.js.map +1 -0
- package/dist/launch/revert.d.ts +23 -0
- package/dist/launch/revert.d.ts.map +1 -0
- package/dist/launch/revert.js +169 -0
- package/dist/launch/revert.js.map +1 -0
- package/dist/launch/salt.d.ts +90 -0
- package/dist/launch/salt.d.ts.map +1 -0
- package/dist/launch/salt.js +153 -0
- package/dist/launch/salt.js.map +1 -0
- package/dist/launch/stockCall.d.ts +122 -0
- package/dist/launch/stockCall.d.ts.map +1 -0
- package/dist/launch/stockCall.js +144 -0
- package/dist/launch/stockCall.js.map +1 -0
- package/dist/launch/tickMath.d.ts +39 -0
- package/dist/launch/tickMath.d.ts.map +1 -0
- package/dist/launch/tickMath.js +109 -0
- package/dist/launch/tickMath.js.map +1 -0
- package/dist/launch/variants.d.ts +64 -0
- package/dist/launch/variants.d.ts.map +1 -0
- package/dist/launch/variants.js +131 -0
- package/dist/launch/variants.js.map +1 -0
- package/dist/launch/wethCall.d.ts +133 -0
- package/dist/launch/wethCall.d.ts.map +1 -0
- package/dist/launch/wethCall.js +173 -0
- package/dist/launch/wethCall.js.map +1 -0
- package/dist/launch/wethPlan.d.ts +71 -0
- package/dist/launch/wethPlan.d.ts.map +1 -0
- package/dist/launch/wethPlan.js +146 -0
- package/dist/launch/wethPlan.js.map +1 -0
- package/dist/perps/abi.generated.d.ts +794 -0
- package/dist/perps/abi.generated.d.ts.map +1 -0
- package/dist/perps/abi.generated.js +738 -0
- package/dist/perps/abi.generated.js.map +1 -0
- package/dist/perps/index.d.ts +160 -0
- package/dist/perps/index.d.ts.map +1 -0
- package/dist/perps/index.js +147 -0
- package/dist/perps/index.js.map +1 -0
- package/package.json +105 -0
- package/src/abis.generated.ts +2738 -0
- package/src/abis.ts +35 -0
- package/src/artDex.ts +254 -0
- package/src/client.ts +10 -0
- package/src/clog.ts +171 -0
- package/src/clogInflow.ts +217 -0
- package/src/deployments.generated.ts +98 -0
- package/src/deployments.ts +60 -0
- package/src/feeSplit.ts +76 -0
- package/src/feeTerms.ts +137 -0
- package/src/index.ts +233 -0
- package/src/launch/abi.generated.ts +2481 -0
- package/src/launch/addresses.ts +58 -0
- package/src/launch/feeTerms.ts +93 -0
- package/src/launch/index.ts +93 -0
- package/src/launch/revert.ts +188 -0
- package/src/launch/salt.ts +216 -0
- package/src/launch/stockCall.ts +223 -0
- package/src/launch/tickMath.ts +139 -0
- package/src/launch/variants.ts +183 -0
- package/src/launch/wethCall.ts +240 -0
- package/src/launch/wethPlan.ts +257 -0
- package/src/perps/abi.generated.ts +741 -0
- package/src/perps/index.ts +263 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Address } from 'viem';
|
|
2
|
+
import { addressOf, deploymentFor } from '../deployments.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The addresses a launch builder needs, per chain. INJECTED, never assumed:
|
|
6
|
+
* every consumer resolves them from the active chain's deployment artifact —
|
|
7
|
+
* the SDK's own address book (`launchAddressesFor`) or any artifact keyed by
|
|
8
|
+
* contract name (`launchAddressesFromRecord`, what the API and the CLI use).
|
|
9
|
+
*
|
|
10
|
+
* `clogVaultFactory` is nullable and that is the point: a deployment made before
|
|
11
|
+
* the Clog launch type existed has no such contract, and a clog type is then
|
|
12
|
+
* refused rather than launched without its vault.
|
|
13
|
+
*/
|
|
14
|
+
export interface LaunchAddresses {
|
|
15
|
+
readonly factory: Address;
|
|
16
|
+
readonly orchestrator: Address;
|
|
17
|
+
readonly liquidityExecutor: Address;
|
|
18
|
+
readonly feeHook: Address;
|
|
19
|
+
readonly stockLaunchpad: Address;
|
|
20
|
+
readonly clogVaultFactory: Address | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ZERO = /^0x0{40}$/i;
|
|
24
|
+
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
25
|
+
|
|
26
|
+
function required(record: Readonly<Record<string, unknown>>, name: string): Address {
|
|
27
|
+
const value = record[name];
|
|
28
|
+
if (typeof value !== 'string' || !ADDRESS.test(value) || ZERO.test(value)) {
|
|
29
|
+
throw new Error(`Launch addresses: ${name} is missing from the deployment record.`);
|
|
30
|
+
}
|
|
31
|
+
return value as Address;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** From a deployment artifact keyed by contract name (`contracts/deployments/<chainId>.json`). */
|
|
35
|
+
export function launchAddressesFromRecord(record: Readonly<Record<string, unknown>>): LaunchAddresses {
|
|
36
|
+
const clog = record['PeddlesClogVaultFactory'];
|
|
37
|
+
return {
|
|
38
|
+
factory: required(record, 'PeddlesFactoryV20'),
|
|
39
|
+
orchestrator: required(record, 'PeddlesLaunchOrchestratorV20'),
|
|
40
|
+
liquidityExecutor: required(record, 'PeddlesV4LiquidityExecutor'),
|
|
41
|
+
feeHook: required(record, 'PeddlesFeeHook'),
|
|
42
|
+
stockLaunchpad: required(record, 'PeddlesStockLaunchpad'),
|
|
43
|
+
clogVaultFactory: typeof clog === 'string' && ADDRESS.test(clog) && !ZERO.test(clog) ? (clog as Address) : null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** From the SDK's shipped address book. Throws `UnknownChainError` for a chain it has none for. */
|
|
48
|
+
export function launchAddressesFor(chainId: number): LaunchAddresses {
|
|
49
|
+
const book = deploymentFor(chainId);
|
|
50
|
+
return {
|
|
51
|
+
factory: addressOf(chainId, 'PeddlesFactoryV20'),
|
|
52
|
+
orchestrator: addressOf(chainId, 'PeddlesLaunchOrchestratorV20'),
|
|
53
|
+
liquidityExecutor: addressOf(chainId, 'PeddlesV4LiquidityExecutor'),
|
|
54
|
+
feeHook: addressOf(chainId, 'PeddlesFeeHook'),
|
|
55
|
+
stockLaunchpad: addressOf(chainId, 'PeddlesStockLaunchpad'),
|
|
56
|
+
clogVaultFactory: launchAddressesFromRecord(book).clogVaultFactory,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The launch fee terms — one fee model for every launch type, chosen at launch
|
|
3
|
+
* and permanent. A pure port of the web app's `features/launch/feeTerms.ts`
|
|
4
|
+
* (the bigint slice the builders need; the render helpers stay in the web).
|
|
5
|
+
*
|
|
6
|
+
* taxBps the all-in tax on every trade, TAX_MIN_BPS..TAX_MAX_BPS
|
|
7
|
+
* excessToCreatorBps of everything ABOVE the first BASE_BPS, the fraction
|
|
8
|
+
* (0..10000) that goes to the creator; the rest to holders
|
|
9
|
+
*
|
|
10
|
+
* Import-free, bigint only. Nothing here is a `number`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 100% in basis points. */
|
|
14
|
+
export const BPS = 10_000n;
|
|
15
|
+
|
|
16
|
+
/** The all-in tax a launch may choose, inclusive. 1.00% .. 10.00%. */
|
|
17
|
+
export const TAX_MIN_BPS = 100n;
|
|
18
|
+
export const TAX_MAX_BPS = 1_000n;
|
|
19
|
+
|
|
20
|
+
/** The first 1.00% of every tax, split evenly and never adjustable. */
|
|
21
|
+
export const BASE_BPS = 100n;
|
|
22
|
+
export const PLATFORM_BASE_BPS = 50n;
|
|
23
|
+
export const CREATOR_BASE_BPS = 50n;
|
|
24
|
+
|
|
25
|
+
/** `excessToCreatorBps` bounds, inclusive. */
|
|
26
|
+
export const EXCESS_MIN_BPS = 0n;
|
|
27
|
+
export const EXCESS_MAX_BPS = BPS;
|
|
28
|
+
|
|
29
|
+
export interface FeeTerms {
|
|
30
|
+
readonly taxBps: bigint;
|
|
31
|
+
readonly excessToCreatorBps: bigint;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type FeeTermsIssue =
|
|
35
|
+
| { readonly field: 'taxBps'; readonly code: 'TAX_BELOW_MIN' | 'TAX_ABOVE_MAX' }
|
|
36
|
+
| { readonly field: 'excessToCreatorBps'; readonly code: 'SPLIT_OUT_OF_RANGE' };
|
|
37
|
+
|
|
38
|
+
/** The first bound a pair of terms breaks, or null when both are in range. */
|
|
39
|
+
export function validateFeeTerms(terms: FeeTerms): FeeTermsIssue | null {
|
|
40
|
+
if (terms.taxBps < TAX_MIN_BPS) return { field: 'taxBps', code: 'TAX_BELOW_MIN' };
|
|
41
|
+
if (terms.taxBps > TAX_MAX_BPS) return { field: 'taxBps', code: 'TAX_ABOVE_MAX' };
|
|
42
|
+
if (terms.excessToCreatorBps < EXCESS_MIN_BPS || terms.excessToCreatorBps > EXCESS_MAX_BPS) {
|
|
43
|
+
return { field: 'excessToCreatorBps', code: 'SPLIT_OUT_OF_RANGE' };
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Per-trade split, each leg in units of `BPS * BPS` (= 100% of volume). Exact, no rounding. */
|
|
49
|
+
export interface FeeSplitRate {
|
|
50
|
+
readonly platform: bigint;
|
|
51
|
+
readonly creator: bigint;
|
|
52
|
+
readonly holders: bigint;
|
|
53
|
+
readonly total: bigint;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The exact per-trade split, or null for terms outside the bounds. */
|
|
57
|
+
export function feeSplitRate(terms: FeeTerms): FeeSplitRate | null {
|
|
58
|
+
if (validateFeeTerms(terms) !== null) return null;
|
|
59
|
+
const excess = terms.taxBps - BASE_BPS;
|
|
60
|
+
const platform = PLATFORM_BASE_BPS * BPS;
|
|
61
|
+
const creator = CREATOR_BASE_BPS * BPS + excess * terms.excessToCreatorBps;
|
|
62
|
+
const holders = excess * (BPS - terms.excessToCreatorBps);
|
|
63
|
+
return { platform, creator, holders, total: platform + creator + holders };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const PERCENT_RE = /^(\d{1,3})(?:\.(\d{0,2}))?$/;
|
|
67
|
+
|
|
68
|
+
/** "2.5" → 250n. At most two decimal places; anything else is null. Bounds are not checked. */
|
|
69
|
+
export function parseTaxPercent(text: string): bigint | null {
|
|
70
|
+
const match = PERCENT_RE.exec(text.trim());
|
|
71
|
+
if (!match) return null;
|
|
72
|
+
const whole = BigInt(match[1]!);
|
|
73
|
+
const frac = BigInt((match[2] ?? '').padEnd(2, '0'));
|
|
74
|
+
return whole * 100n + frac;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 250n → "2.50". Integer bps only; the percent sign is the caller's. */
|
|
78
|
+
export function formatTaxPercent(bps: bigint): string {
|
|
79
|
+
const sign = bps < 0n ? '-' : '';
|
|
80
|
+
const abs = bps < 0n ? -bps : bps;
|
|
81
|
+
return `${sign}${abs / 100n}.${(abs % 100n).toString().padStart(2, '0')}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A `FeeSplitRate` leg as a percent string with up to four decimals, for the render edge only. */
|
|
85
|
+
export function formatRatePercent(rate: bigint): string {
|
|
86
|
+
// rate / (BPS*BPS) * 100 = rate / 1_000_000 → four decimals of a percent.
|
|
87
|
+
const unit = BPS * BPS / 100n / 10_000n; // one 0.0001% in rate units
|
|
88
|
+
const units = rate / unit;
|
|
89
|
+
const whole = units / 10_000n;
|
|
90
|
+
let frac = (units % 10_000n).toString().padStart(4, '0');
|
|
91
|
+
while (frac.length > 2 && frac.endsWith('0')) frac = frac.slice(0, -1);
|
|
92
|
+
return `${whole}.${frac}`;
|
|
93
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @peddles/sdk/launch — build a Peddles launch transaction, non-custodially.
|
|
3
|
+
*
|
|
4
|
+
* Pure builders ported from the web app's launch forms and held identical by
|
|
5
|
+
* test: the same salt miner, plan, input and call assembly the forms sign, so
|
|
6
|
+
* the REST API, the CLI, the widget and an AI agent all produce EXACTLY the bytes
|
|
7
|
+
* a creator would have signed in the app.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here holds a key or sends a transaction. Every function returns data
|
|
10
|
+
* (`to`, `data`, `value`) for something else to sign. Every address, decimal,
|
|
11
|
+
* tick and fee is read from the active chain through the `PublicClient` you pass
|
|
12
|
+
* in; nothing is a constant.
|
|
13
|
+
*/
|
|
14
|
+
export {
|
|
15
|
+
ADDRESS_SUFFIX,
|
|
16
|
+
ZERO_SALT,
|
|
17
|
+
boundSalt,
|
|
18
|
+
create2Address,
|
|
19
|
+
hasSuffix,
|
|
20
|
+
randomSalt,
|
|
21
|
+
mineSalt,
|
|
22
|
+
MiningAbortedError,
|
|
23
|
+
MiningExhaustedError,
|
|
24
|
+
} from './salt.js';
|
|
25
|
+
export type { MineParams, MinedSalt } from './salt.js';
|
|
26
|
+
|
|
27
|
+
export { MIN_TICK, MAX_TICK, Q96, getSqrtPriceAtTick, amountForLiquidity, liquidityForAmount, startSqrtPrice } from './tickMath.js';
|
|
28
|
+
export type { LiquidityQuote } from './tickMath.js';
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
BPS,
|
|
32
|
+
TAX_MIN_BPS,
|
|
33
|
+
TAX_MAX_BPS,
|
|
34
|
+
BASE_BPS,
|
|
35
|
+
PLATFORM_BASE_BPS,
|
|
36
|
+
CREATOR_BASE_BPS,
|
|
37
|
+
EXCESS_MIN_BPS,
|
|
38
|
+
EXCESS_MAX_BPS,
|
|
39
|
+
validateFeeTerms,
|
|
40
|
+
feeSplitRate,
|
|
41
|
+
parseTaxPercent,
|
|
42
|
+
formatTaxPercent,
|
|
43
|
+
formatRatePercent,
|
|
44
|
+
} from './feeTerms.js';
|
|
45
|
+
export type { FeeTerms, FeeTermsIssue, FeeSplitRate } from './feeTerms.js';
|
|
46
|
+
|
|
47
|
+
export { launchAddressesFor, launchAddressesFromRecord } from './addresses.js';
|
|
48
|
+
export type { LaunchAddresses } from './addresses.js';
|
|
49
|
+
|
|
50
|
+
export { VARIANT_ASSET, BPS_DENOMINATOR, resolveTerms, readLaunchVariants, variantForType } from './variants.js';
|
|
51
|
+
export type { VariantAllocation, LaunchTypeShape, ResolvedTerms, LaunchVariant, LaunchTypeName } from './variants.js';
|
|
52
|
+
|
|
53
|
+
export { SUPPLY_UNITS, LAUNCH_COIN_DECIMALS, buildWethLaunchPlan } from './wethPlan.js';
|
|
54
|
+
export type { WethLaunchPlan, WethPlanArgs } from './wethPlan.js';
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
assetParamsV1Components,
|
|
58
|
+
EMPTY_WETH_METADATA,
|
|
59
|
+
buildInitCalls,
|
|
60
|
+
wethLaunchTermsIssue,
|
|
61
|
+
buildWethLaunchInput,
|
|
62
|
+
buildWethLaunchCall,
|
|
63
|
+
encodeWethLaunchCall,
|
|
64
|
+
orchestratorLaunchFeeAbi,
|
|
65
|
+
readOrchestratorLaunchFee,
|
|
66
|
+
} from './wethCall.js';
|
|
67
|
+
export type { WethMetadata, WethLaunchArgs, WethLaunchInput, WethLaunchCall, UnsignedLaunchTx, OrchestratorLaunchFee } from './wethCall.js';
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
stockLaunchTermsIssue,
|
|
71
|
+
buildStockLaunchCall,
|
|
72
|
+
encodeStockLaunchCall,
|
|
73
|
+
encodeStockApproval,
|
|
74
|
+
erc20ApprovalAbi,
|
|
75
|
+
readStockAllowance,
|
|
76
|
+
readStockLaunchFee,
|
|
77
|
+
readStockMiningInputs,
|
|
78
|
+
predictStockCoin,
|
|
79
|
+
readStockQuotes,
|
|
80
|
+
isQuoteAllowed,
|
|
81
|
+
} from './stockCall.js';
|
|
82
|
+
export type { StockLaunchArgs, StockLaunchCall, StockLaunchFee, StockMiningInputs, StockQuote } from './stockCall.js';
|
|
83
|
+
|
|
84
|
+
export { LAUNCH_REVERT_REASONS, REVERTED_ON_CHAIN, revertDataOf, revertIdentifier, explainLaunchRevert, decodeLaunchRevert } from './revert.js';
|
|
85
|
+
export type { DecodedLaunchRevert } from './revert.js';
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
launchOrchestratorAbi,
|
|
89
|
+
stockLaunchpadLaunchAbi,
|
|
90
|
+
liquidityExecutorLaunchAbi,
|
|
91
|
+
tokenMetadataAbi,
|
|
92
|
+
launchErrorsAbi,
|
|
93
|
+
} from './abi.generated.js';
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { decodeErrorResult, isHex } from 'viem';
|
|
2
|
+
import type { Hex } from 'viem';
|
|
3
|
+
import { launchErrorsAbi } from './abi.generated.js';
|
|
4
|
+
import { TAX_MAX_BPS, TAX_MIN_BPS, formatTaxPercent } from './feeTerms.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Turn a launch revert into a sentence someone can act on — a port of the web
|
|
8
|
+
* app's `features/launch/txReason.ts`, minus the wallet-specific cases.
|
|
9
|
+
*
|
|
10
|
+
* Every entry maps to a real `require` string or custom error in `contracts/src/`.
|
|
11
|
+
* The table is keyed by the contract's own identifier, so a renamed error stops
|
|
12
|
+
* matching and falls through to the honest generic case. An unrecognised revert
|
|
13
|
+
* returns `raw: null` — a confident wrong reason is worse than "the contract
|
|
14
|
+
* rejected this", because the user acts on it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const TAX_MIN = formatTaxPercent(TAX_MIN_BPS);
|
|
18
|
+
const TAX_MAX = formatTaxPercent(TAX_MAX_BPS);
|
|
19
|
+
|
|
20
|
+
export const LAUNCH_REVERT_REASONS: Readonly<Record<string, string>> = {
|
|
21
|
+
// ---- PeddlesStockLaunchpad -------------------------------------------------
|
|
22
|
+
POOL_PRICE_MISMATCH:
|
|
23
|
+
'A Uniswap pool for this exact token address and quote already exists at a different price, so the launch cannot initialise it. Prepare again — a fresh salt moves the token to an unused address.',
|
|
24
|
+
QUOTE_NOT_ALLOWED: 'The launchpad does not allow this quote asset. Pick one from the list — anything else fails.',
|
|
25
|
+
LAUNCH_FEE_REQUIRED:
|
|
26
|
+
'The transaction did not carry the launchpad’s launch fee. The fee is read live before signing, so it changed between the quote and the signature — prepare again.',
|
|
27
|
+
NoDevBuy: 'A minimum-tokens-out was set with a dev-buy amount of zero. Either enter an amount to spend, or clear the slippage floor.',
|
|
28
|
+
NoSwapRouter: 'The launchpad has no swap router configured, so it cannot perform a dev buy. Launch without a dev buy, then buy once the pool is live.',
|
|
29
|
+
DevBuySlippage: 'The dev buy would have returned fewer tokens than your minimum. Lower the minimum-tokens-out, or reduce the amount you are spending.',
|
|
30
|
+
QuotePullFailed: 'The quote token could not be pulled from your wallet. Check the approval covers the full dev-buy amount and that your balance still holds it.',
|
|
31
|
+
QuoteApproveFailed: 'The launchpad could not approve the swap router to spend the quote asset. Try a smaller dev buy or launch without one.',
|
|
32
|
+
QuoteRefundFailed: 'The unspent part of the dev buy could not be returned. Nothing was launched.',
|
|
33
|
+
TICK_RANGE: 'The opening price configured for this quote asset is outside the range a pool can hold.',
|
|
34
|
+
TickRange: 'The opening price configured for this quote asset is outside the range a pool can hold.',
|
|
35
|
+
TickLow: 'The opening price configured for this quote asset is outside the range a pool can hold.',
|
|
36
|
+
TickHigh: 'The opening price configured for this quote asset is outside the range a pool can hold.',
|
|
37
|
+
TICK_SPACING: 'The opening price configured for this quote asset is not one this pool can open at.',
|
|
38
|
+
ZERO_QUOTE: 'No quote asset was selected.',
|
|
39
|
+
POOL_INIT_FAILED: 'The pool could not be opened with these settings.',
|
|
40
|
+
HolderRewardsBinding: 'The token’s holder rewards could not be connected during the launch, so nothing launched.',
|
|
41
|
+
ZERO_FEE_HOOK: 'The launchpad has no fee hook, so it cannot open a pool. Nothing launched.',
|
|
42
|
+
UNKNOWN_COIN: 'This token was not launched through this launchpad.',
|
|
43
|
+
|
|
44
|
+
// ---- Launched tokens ------------------------------------------------------
|
|
45
|
+
NotifyGasTooLow: 'The transaction ran out of gas for this token’s reward update, so nothing moved. Try again and let your wallet estimate the gas.',
|
|
46
|
+
|
|
47
|
+
// ---- PeddlesFeeHook ---------------------------------------------------------
|
|
48
|
+
FeeBelowFloor: `The trading fee is below the ${TAX_MIN}% minimum. Choose a fee between ${TAX_MIN}% and ${TAX_MAX}%.`,
|
|
49
|
+
FeeAboveCap: `The trading fee is above the ${TAX_MAX}% maximum, or the split is outside all to holders and all to you. Choose a fee between ${TAX_MIN}% and ${TAX_MAX}%.`,
|
|
50
|
+
AlreadyRegistered: 'A pool for this token and quote is already set up, so this launch cannot open it.',
|
|
51
|
+
NotRegistered: 'This pool has no Peddles trading fee set up.',
|
|
52
|
+
|
|
53
|
+
// ---- PeddlesV4SwapRouter ------------------------------------------------------
|
|
54
|
+
FeeStackingForbidden: 'This pool already charges its trading fee, so the router refuses to add a second fee on top.',
|
|
55
|
+
Slippage: 'The buy would return fewer tokens than your minimum, so it would fail. Lower the minimum or the amount.',
|
|
56
|
+
|
|
57
|
+
// ---- PeddlesLaunchOrchestratorV20 (WETH path) ------------------------------
|
|
58
|
+
UNSUPPORTED_VARIANT: 'Peddles does not support this kind of token on this network.',
|
|
59
|
+
CREATOR_MISMATCH: 'The launch was built for a different creator address than the wallet that signed it. Prepare it again for the signing wallet.',
|
|
60
|
+
BAD_FACTORY_VALUE: 'The transaction carried less value than the launch declared.',
|
|
61
|
+
BAD_DEV_BUY_VALUE: 'The transaction carried less value than the first buy. Nothing launched.',
|
|
62
|
+
NO_DEV_BUY: 'A minimum was set with no first buy. Enter an amount to spend, or clear the minimum.',
|
|
63
|
+
NO_SWAP_ROUTER: 'The launch has no swap router to make the first buy through. Launch without a first buy, then buy once the pool is live.',
|
|
64
|
+
DEV_BUY_SLIPPAGE: 'The first buy would return fewer tokens than your minimum, so nothing launched. Lower the minimum or the amount.',
|
|
65
|
+
SUPPLY_MUST_BE_ONE_BILLION: 'Every Peddles launch is exactly 1,000,000,000 tokens. The supply this launch declared does not match.',
|
|
66
|
+
BPS_SUM: 'The allocation legs do not sum to 100%.',
|
|
67
|
+
NO_LAUNCH_BURN: 'Peddles refuses any launch that burns supply.',
|
|
68
|
+
BAD_LIQUIDITY_INPUT: 'The pool settings this launch worked out were rejected. Prepare again so they are recalculated from the current on-chain configuration.',
|
|
69
|
+
BAD_POOL_PARAMS: 'The pool price or liquidity did not match what the launch expects. Prepare again to recalculate them.',
|
|
70
|
+
TOKEN_PREDICTION_MISMATCH: 'The token did not deploy at the address Peddles previewed. Nothing launched; prepare again with a fresh salt.',
|
|
71
|
+
CLOG_NOT_ON_GRADUATION: 'A collection cannot launch a coin that holds supply back.',
|
|
72
|
+
|
|
73
|
+
// ---- PeddlesClogVault ---------------------------------------------------------
|
|
74
|
+
FloorUnset: 'The creator has not set a minimum price, so only the creator can sell the next slice.',
|
|
75
|
+
TooSoon: 'The next slice cannot be sold yet. Try again once the interval has passed.',
|
|
76
|
+
NotCreator: 'Only this coin’s creator can do this.',
|
|
77
|
+
|
|
78
|
+
// ---- shared ---------------------------------------------------------------
|
|
79
|
+
REENTRANT: 'The contract rejected a nested call for safety. Nothing moved.',
|
|
80
|
+
Reentrant: 'The contract rejected a nested call for safety. Nothing moved.',
|
|
81
|
+
ReentrantCall: 'The contract rejected a nested call for safety. Nothing moved.',
|
|
82
|
+
NOT_OWNER: 'Your wallet is not the owner of this contract.',
|
|
83
|
+
NotOwner: 'Your wallet is not the owner of this contract.',
|
|
84
|
+
Unauthorized: 'Your wallet is not authorised to make this call.',
|
|
85
|
+
ZERO_ADDRESS: 'A required address was zero.',
|
|
86
|
+
ZeroAddress: 'A required address was zero.',
|
|
87
|
+
REFUND_FAILED: 'The excess amount could not be refunded, so the whole transaction failed.',
|
|
88
|
+
RefundFailed: 'The excess amount could not be refunded, so the whole transaction failed.',
|
|
89
|
+
ALLOWANCE: 'The approval on the quote asset does not cover this amount.',
|
|
90
|
+
BALANCE: 'Your balance of the quote asset is lower than the amount this call needs.',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export interface DecodedLaunchRevert {
|
|
94
|
+
/** The contract's own error identifier (e.g. `POOL_PRICE_MISMATCH`), or null when unrecognised. */
|
|
95
|
+
readonly raw: string | null;
|
|
96
|
+
/** A sentence, always present. */
|
|
97
|
+
readonly message: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const ERROR_STRING_SELECTOR = '0x08c379a0';
|
|
101
|
+
const PANIC_SELECTOR = '0x4e487b71';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Every error in a `cause` chain, top first. DUCK-TYPED, not `instanceof BaseError`: a consumer
|
|
105
|
+
* with its own copy of viem (a monorepo app, a bundler that did not dedupe) throws errors from a
|
|
106
|
+
* different class object, and an `instanceof` check would silently decode nothing.
|
|
107
|
+
*/
|
|
108
|
+
function causeChain(error: unknown): readonly Record<string, unknown>[] {
|
|
109
|
+
const out: Record<string, unknown>[] = [];
|
|
110
|
+
let current: unknown = error;
|
|
111
|
+
for (let depth = 0; depth < 16 && current && typeof current === 'object'; depth += 1) {
|
|
112
|
+
out.push(current as Record<string, unknown>);
|
|
113
|
+
current = (current as { cause?: unknown }).cause;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const isHexData = (value: unknown): value is Hex => typeof value === 'string' && isHex(value) && value.length >= 10;
|
|
119
|
+
|
|
120
|
+
/** The revert data hex carried anywhere in a viem error chain, or null. */
|
|
121
|
+
export function revertDataOf(error: unknown): Hex | null {
|
|
122
|
+
for (const e of causeChain(error)) {
|
|
123
|
+
if (isHexData(e['raw'])) return e['raw'];
|
|
124
|
+
if (isHexData(e['data'])) return e['data'];
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The identifier revert data names: an `Error(string)` reason or a custom error's name. */
|
|
130
|
+
export function revertIdentifier(data: Hex): string | null {
|
|
131
|
+
const selector = data.slice(0, 10).toLowerCase();
|
|
132
|
+
try {
|
|
133
|
+
if (selector === ERROR_STRING_SELECTOR) {
|
|
134
|
+
const decoded = decodeErrorResult({ abi: [{ type: 'error', name: 'Error', inputs: [{ type: 'string' }] }], data });
|
|
135
|
+
const reason = String(decoded.args?.[0] ?? '').trim();
|
|
136
|
+
return reason.length > 0 ? reason : null;
|
|
137
|
+
}
|
|
138
|
+
if (selector === PANIC_SELECTOR) return 'Panic';
|
|
139
|
+
const decoded = decodeErrorResult({ abi: launchErrorsAbi, data });
|
|
140
|
+
return decoded.errorName;
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The mapped sentence for a contract identifier, or null when unrecognised. */
|
|
147
|
+
export function explainLaunchRevert(identifier: string | null): string | null {
|
|
148
|
+
if (!identifier) return null;
|
|
149
|
+
return LAUNCH_REVERT_REASONS[identifier] ?? null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Decode an RPC/contract failure into `{ raw, message }`. Accepts a viem error
|
|
154
|
+
* (from `simulateContract`, `call` or `sendTransaction`), any error whose cause
|
|
155
|
+
* chain carries revert data, or raw revert data.
|
|
156
|
+
*/
|
|
157
|
+
export function decodeLaunchRevert(error: unknown): DecodedLaunchRevert {
|
|
158
|
+
const data = isHexData(error) ? error : revertDataOf(error);
|
|
159
|
+
// A viem `ContractFunctionRevertedError` already carries the reason / errorName; take it from
|
|
160
|
+
// wherever in the chain it sits. An `Error(string)` reason is the identifier itself.
|
|
161
|
+
let identifier: string | null = null;
|
|
162
|
+
for (const e of causeChain(error)) {
|
|
163
|
+
const reason = e['reason'];
|
|
164
|
+
if (typeof reason === 'string' && reason.trim()) {
|
|
165
|
+
identifier = reason.trim();
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
const nested = e['data'];
|
|
169
|
+
const errorName = nested && typeof nested === 'object' ? (nested as { errorName?: unknown }).errorName : undefined;
|
|
170
|
+
if (typeof errorName === 'string' && errorName !== 'Error') {
|
|
171
|
+
identifier = errorName;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (identifier === null && data) identifier = revertIdentifier(data);
|
|
176
|
+
|
|
177
|
+
const mapped = explainLaunchRevert(identifier);
|
|
178
|
+
if (mapped) return { raw: identifier, message: mapped };
|
|
179
|
+
if (identifier) return { raw: identifier, message: 'The contract rejected these settings.' };
|
|
180
|
+
const top = causeChain(error)[0];
|
|
181
|
+
const short = top && typeof top['shortMessage'] === 'string' ? (top['shortMessage'] as string) : null;
|
|
182
|
+
if (short) return { raw: null, message: short };
|
|
183
|
+
return { raw: null, message: error instanceof Error ? error.message : 'The transaction could not be simulated.' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A mined-but-reverted receipt carries no reason — the node discarded it. */
|
|
187
|
+
export const REVERTED_ON_CHAIN =
|
|
188
|
+
'The transaction was mined and the contract rejected it. Gas was spent; nothing else moved. Open it on the explorer for the failing call.';
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { concat, encodeAbiParameters, getAddress, keccak256 } from 'viem';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Salt handling for the two CREATE2 launch paths — a pure port of the web app's
|
|
5
|
+
* `features/launch/salt.ts`, held identical by test.
|
|
6
|
+
*
|
|
7
|
+
* ─── EVERY PEDDLES ADDRESS ENDS IN `1978` ────────────────────────────────────
|
|
8
|
+
* The platform contracts do, and launched tokens do too, with no option to choose
|
|
9
|
+
* otherwise. So there is no vanity field: a launch MINES a salt whose address
|
|
10
|
+
* carries the suffix, and the creator sees the address they will get.
|
|
11
|
+
*
|
|
12
|
+
* A salt does two jobs, and only one of them is the suffix:
|
|
13
|
+
*
|
|
14
|
+
* 1. It picks the address. Both contracts mix the variant AND `msg.sender`
|
|
15
|
+
* into it (`keccak256(abi.encode(variant, sender, salt))`), so a salt only
|
|
16
|
+
* ever addresses the caller's OWN CREATE2 space.
|
|
17
|
+
* 2. It defeats front-running. The token address is an input to the pool key,
|
|
18
|
+
* so anyone who can predict it can open that pool first at a price of their
|
|
19
|
+
* choosing — the launch then reverts `POOL_PRICE_MISMATCH`. Mining starts
|
|
20
|
+
* from a CSPRNG value, so the mined salt is as unguessable as a random one.
|
|
21
|
+
*
|
|
22
|
+
* ─── LOCAL DERIVATION, CONTRACT CONFIRMATION ─────────────────────────────────
|
|
23
|
+
* Mining computes the address locally (65,536 expected attempts is not an RPC
|
|
24
|
+
* job), and the plan builders then CONFIRM the candidate with the contract's own
|
|
25
|
+
* `getPeddlesAddress` / `predictCoin` before it is shown or submitted.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** The required address suffix, lowercase hex, no `0x`. */
|
|
29
|
+
export const ADDRESS_SUFFIX = '1978';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `keccak256(abi.encode(uint8 variant, address sender, bytes32 salt))` — the
|
|
33
|
+
* salt binding both `PeddlesStockLaunchpad._boundSalt` and
|
|
34
|
+
* `PeddlesFactoryV20._boundSalt` apply.
|
|
35
|
+
*/
|
|
36
|
+
export function boundSalt(variant: number, sender: `0x${string}`, salt: `0x${string}`): `0x${string}` {
|
|
37
|
+
return keccak256(
|
|
38
|
+
encodeAbiParameters(
|
|
39
|
+
[{ type: 'uint8' }, { type: 'address' }, { type: 'bytes32' }],
|
|
40
|
+
[variant, sender, salt],
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Standard CREATE2: `keccak256(0xff ‖ deployer ‖ salt ‖ initCodeHash)[12:]`. */
|
|
46
|
+
export function create2Address(
|
|
47
|
+
deployer: `0x${string}`,
|
|
48
|
+
salt: `0x${string}`,
|
|
49
|
+
initCodeHash: `0x${string}`,
|
|
50
|
+
): `0x${string}` {
|
|
51
|
+
const digest = keccak256(concat(['0xff', deployer, salt, initCodeHash]));
|
|
52
|
+
return getAddress(`0x${digest.slice(26)}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function hasSuffix(address: string, suffix: string = ADDRESS_SUFFIX): boolean {
|
|
56
|
+
return address.toLowerCase().endsWith(suffix.toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A fresh unguessable salt from the platform CSPRNG (`crypto.getRandomValues`,
|
|
61
|
+
* never `Math.random`): this value is the only thing standing between a launch
|
|
62
|
+
* and a mempool watcher who opens the pool first.
|
|
63
|
+
*/
|
|
64
|
+
export function randomSalt(): `0x${string}` {
|
|
65
|
+
const bytes = new Uint8Array(32);
|
|
66
|
+
// Typed structurally: this package ships no DOM or Node lib, and both runtimes expose the same API.
|
|
67
|
+
const cryptoApi = (globalThis as unknown as { crypto?: { getRandomValues(array: Uint8Array): Uint8Array } }).crypto;
|
|
68
|
+
if (!cryptoApi) throw new Error('No CSPRNG (globalThis.crypto) is available; refusing to mine a guessable salt.');
|
|
69
|
+
cryptoApi.getRandomValues(bytes);
|
|
70
|
+
return `0x${Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `0x00…00` — the salt a caller means by "none", still bound to the sender. */
|
|
74
|
+
export const ZERO_SALT: `0x${string}` = `0x${'0'.repeat(64)}`;
|
|
75
|
+
|
|
76
|
+
export interface MineParams {
|
|
77
|
+
/** The contract that runs CREATE2 — the launchpad or the factory. */
|
|
78
|
+
readonly deployer: `0x${string}`;
|
|
79
|
+
/** The variant byte the contract mixes into the salt. */
|
|
80
|
+
readonly variant: number;
|
|
81
|
+
/** The `msg.sender` the contract binds the salt to — the creator, or the orchestrator. */
|
|
82
|
+
readonly sender: `0x${string}`;
|
|
83
|
+
readonly initCodeHash: `0x${string}`;
|
|
84
|
+
readonly suffix?: string;
|
|
85
|
+
/** Anything with an `aborted` flag — an `AbortSignal`, or a plain object a caller flips. */
|
|
86
|
+
readonly signal?: { readonly aborted: boolean };
|
|
87
|
+
/** Called every chunk with the attempt count so far. */
|
|
88
|
+
readonly onProgress?: (attempts: number) => void;
|
|
89
|
+
/**
|
|
90
|
+
* Hard ceiling on attempts. A 4-hex suffix is 1 in 65,536; the default is ~60×
|
|
91
|
+
* that, so hitting it means something is wrong with the inputs, not bad luck,
|
|
92
|
+
* and the caller fails closed rather than looping forever.
|
|
93
|
+
*/
|
|
94
|
+
readonly maxAttempts?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface MinedSalt {
|
|
98
|
+
readonly salt: `0x${string}`;
|
|
99
|
+
/** The locally derived address. Confirm it with the contract before use. */
|
|
100
|
+
readonly address: `0x${string}`;
|
|
101
|
+
readonly attempts: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class MiningAbortedError extends Error {
|
|
105
|
+
constructor() {
|
|
106
|
+
super('Address mining was cancelled.');
|
|
107
|
+
this.name = 'MiningAbortedError';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class MiningExhaustedError extends Error {
|
|
112
|
+
readonly attempts: number;
|
|
113
|
+
constructor(suffix: string, attempts: number) {
|
|
114
|
+
super(`No address ending in ${suffix} was found after ${attempts.toLocaleString()} attempts.`);
|
|
115
|
+
this.name = 'MiningExhaustedError';
|
|
116
|
+
this.attempts = attempts;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const CHUNK = 4096;
|
|
121
|
+
|
|
122
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
123
|
+
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
124
|
+
const out = new Uint8Array(clean.length / 2);
|
|
125
|
+
for (let i = 0; i < out.length; i += 1) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function bytesToHex(bytes: Uint8Array): `0x${string}` {
|
|
130
|
+
let s = '0x';
|
|
131
|
+
for (const b of bytes) s += b.toString(16).padStart(2, '0');
|
|
132
|
+
return s as `0x${string}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Find a salt whose bound CREATE2 address ends in `suffix`.
|
|
137
|
+
*
|
|
138
|
+
* The hot loop works on bytes: two keccaks per attempt, preimages preallocated
|
|
139
|
+
* once, the suffix compared on the raw hash bytes. The winner is re-derived
|
|
140
|
+
* through `boundSalt` + `create2Address` before it is returned, so the fast path
|
|
141
|
+
* can never quietly disagree with the readable one. Runs in chunks and yields
|
|
142
|
+
* between them, so a caller can cancel through `signal` and a main thread keeps
|
|
143
|
+
* painting.
|
|
144
|
+
*/
|
|
145
|
+
export async function mineSalt(params: MineParams): Promise<MinedSalt> {
|
|
146
|
+
const {
|
|
147
|
+
deployer,
|
|
148
|
+
variant,
|
|
149
|
+
sender,
|
|
150
|
+
initCodeHash,
|
|
151
|
+
suffix = ADDRESS_SUFFIX,
|
|
152
|
+
signal,
|
|
153
|
+
onProgress,
|
|
154
|
+
maxAttempts = 4_000_000,
|
|
155
|
+
} = params;
|
|
156
|
+
|
|
157
|
+
if (!/^[0-9a-fA-F]+$/.test(suffix) || suffix.length % 2 !== 0) {
|
|
158
|
+
throw new Error(`The address suffix must be whole hex bytes; got "${suffix}".`);
|
|
159
|
+
}
|
|
160
|
+
const want = hexToBytes(suffix);
|
|
161
|
+
|
|
162
|
+
// abi.encode(uint8 variant, address sender, bytes32 salt): three 32-byte words.
|
|
163
|
+
const bound = new Uint8Array(96);
|
|
164
|
+
bound[31] = variant & 0xff;
|
|
165
|
+
bound.set(hexToBytes(sender), 32 + 12);
|
|
166
|
+
const saltView = bound.subarray(64, 96);
|
|
167
|
+
saltView.set(hexToBytes(randomSalt()));
|
|
168
|
+
|
|
169
|
+
// 0xff ‖ deployer ‖ boundSalt ‖ initCodeHash.
|
|
170
|
+
const create2 = new Uint8Array(85);
|
|
171
|
+
create2[0] = 0xff;
|
|
172
|
+
create2.set(hexToBytes(deployer), 1);
|
|
173
|
+
create2.set(hexToBytes(initCodeHash), 53);
|
|
174
|
+
|
|
175
|
+
const bump = () => {
|
|
176
|
+
for (let i = 31; i >= 0; i -= 1) {
|
|
177
|
+
saltView[i] = (saltView[i]! + 1) & 0xff;
|
|
178
|
+
if (saltView[i] !== 0) break;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
let attempts = 0;
|
|
183
|
+
|
|
184
|
+
while (attempts < maxAttempts) {
|
|
185
|
+
if (signal?.aborted) throw new MiningAbortedError();
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i < CHUNK && attempts < maxAttempts; i += 1) {
|
|
188
|
+
create2.set(keccak256(bound, 'bytes'), 21);
|
|
189
|
+
const digest = keccak256(create2, 'bytes');
|
|
190
|
+
attempts += 1;
|
|
191
|
+
|
|
192
|
+
let hit = true;
|
|
193
|
+
for (let j = 0; j < want.length; j += 1) {
|
|
194
|
+
if (digest[32 - want.length + j] !== want[j]) {
|
|
195
|
+
hit = false;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (hit) {
|
|
200
|
+
const salt = bytesToHex(saltView);
|
|
201
|
+
const address = create2Address(deployer, boundSalt(variant, sender, salt), initCodeHash);
|
|
202
|
+
if (!hasSuffix(address, suffix)) {
|
|
203
|
+
throw new Error('The address miner disagreed with itself; nothing was reserved.');
|
|
204
|
+
}
|
|
205
|
+
return { salt, address, attempts };
|
|
206
|
+
}
|
|
207
|
+
bump();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
onProgress?.(attempts);
|
|
211
|
+
// A macrotask yield (`setTimeout(0)`), so a main-thread caller gets to paint between chunks.
|
|
212
|
+
await new Promise<void>((resolve) => (globalThis as unknown as { setTimeout(fn: () => void, ms: number): unknown }).setTimeout(resolve, 0));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
throw new MiningExhaustedError(suffix, attempts);
|
|
216
|
+
}
|