=> {
- const t0 = Transaction.fromLumosSkeleton(tx);
- const size0 = t0.toBytes().length; // +4
- const t1 = await signer.prepareTransaction(t0);
- const size1 = t1.toBytes().length; // +4
- return size1 - size0;
- };
-
- const sendSigned = async (
- tx: TransactionSkeletonType,
- ): Promise<`0x${string}`> =>
- signer.sendTransaction(Transaction.fromLumosSkeleton(tx));
-
return {
...rootConfig,
- address,
+ signer,
+ address: recommendedAddressObj.toString(),
accountLocks,
- expander,
- getTxSizeOverhead,
- sendSigned,
+ primaryLock: accountLocks[0] ?? ccc.Script.from(recommendedAddressObj.script),
};
},
});
- if (isPending)
+ if (isPending) {
return (
<>
>
);
+ }
if (error) {
- setTimeout(function () {
- console.log(error);
- location.reload();
- }, 10000);
- return Unable to connect to {walletName} ⚠️
;
+ return Unable to connect to {walletName}: {errorMessageOf(error)}
;
}
return ;
diff --git a/apps/interface/src/Dashboard.tsx b/apps/interface/src/Dashboard.tsx
index 59146ec..72b25d0 100644
--- a/apps/interface/src/Dashboard.tsx
+++ b/apps/interface/src/Dashboard.tsx
@@ -24,6 +24,7 @@ export function Dashboard({
{"{"}
diff --git a/apps/interface/src/Form.tsx b/apps/interface/src/Form.tsx
index 05633c3..f40e164 100644
--- a/apps/interface/src/Form.tsx
+++ b/apps/interface/src/Form.tsx
@@ -1,19 +1,19 @@
+import type { JSX } from "react";
+import { IckbSdk, type SystemState } from "@ickb/sdk";
import {
- calculateOrderRatio,
+ CKB,
direction2Symbol,
- calculateOrderResult,
reservedCKB,
+ sanitizeAmountInput,
symbol2Direction,
toText,
} from "./utils.ts";
-import { CKB, max, min, type I8Header } from "@ickb/lumos-utils";
-import type { JSX } from "react";
export default function Form({
rawText,
setRawText,
amount,
- tipHeader,
+ system,
isFrozen,
ckbNative,
ickbNative,
@@ -23,9 +23,9 @@ export default function Form({
ickbBalance,
}: {
rawText: string;
- setRawText: (s: string) => void;
+ setRawText: (value: string) => void;
amount: bigint;
- tipHeader: I8Header;
+ system: SystemState;
isFrozen: boolean;
ckbNative: bigint;
ickbNative: bigint;
@@ -34,18 +34,20 @@ export default function Form({
ckbAvailable: bigint;
ickbAvailable: bigint;
}): JSX.Element {
- const symbol = rawText[0];
+ const symbol = rawText.startsWith("I") ? "I" : direction2Symbol(true);
const text = rawText.slice(1);
const isCkb2Udt = symbol2Direction(symbol);
const toggle = (): void => {
setRawText(direction2Symbol(!isCkb2Udt) + text);
};
- const nnn = min(max(ckbAvailable - reservedCKB, 0n), ckbNative);
+ const spendableCkb = ckbAvailable > reservedCKB ? ckbAvailable - reservedCKB : 0n;
+ const nativeCkb = spendableCkb < ckbNative ? spendableCkb : ckbNative;
+
let a = {
name: "CKB",
- native: nnn,
- locked: ckbBalance - nnn,
+ native: nativeCkb,
+ locked: ckbBalance - nativeCkb,
status:
ckbBalance === ckbNative
? "✅"
@@ -54,7 +56,7 @@ export default function Form({
: "⏳",
};
let b = {
- name: "ICKB",
+ name: "iCKB",
native: ickbNative,
locked: ickbBalance - ickbNative,
status:
@@ -69,48 +71,40 @@ export default function Form({
}
return (
- <>
-
- {display(a.native, "✅")}
- {a.name}
-
- {display(a.locked, a.status)}
-
- {
- setRawText(symbol + e.target.value);
- }}
- autoComplete="off"
- inputMode="decimal"
- type="text"
- className="col-span-3 w-full rounded border-0 bg-transparent text-center text-3xl text-amber-400 outline-none disabled:cursor-default"
- aria-label="Amount to be converted"
- />
- {"1 " + a.name}
-
-
- {approxConversion(isCkb2Udt, CKB, tipHeader)} {b.name}
-
-
- ⏳{approxConversion(isCkb2Udt, amount, tipHeader)}
-
- {display(b.native, "✅")}
- {b.name}
-
- {display(b.locked, b.status)}
-
+
+ {display(a.native, "✅")}
+ {a.name}
+ {display(a.locked, a.status)}
+ {
+ setRawText(symbol + sanitizeAmountInput(event.target.value));
+ }}
+ autoComplete="off"
+ inputMode="decimal"
+ type="text"
+ className="col-span-3 w-full rounded border-0 bg-transparent text-center text-3xl text-amber-400 outline-none disabled:cursor-default"
+ aria-label="Amount to be converted"
+ />
+ {`1 ${a.name}`}
+
+ {approxConversion(isCkb2Udt, CKB, system)} {b.name}
+
+ ⏳{approxConversion(isCkb2Udt, amount, system)}
- >
+ {display(b.native, "✅")}
+ {b.name}
+ {display(b.locked, b.status)}
+
);
}
@@ -131,14 +125,13 @@ function display(shannons: bigint, prefix: string): JSX.Element {
function approxConversion(
isCkb2Udt: boolean,
amount: bigint,
- tipHeader: I8Header,
+ system: SystemState,
): string {
- //Worst case scenario is a 0.001% fee for bot
return toText(
- calculateOrderResult(
+ IckbSdk.estimate(
isCkb2Udt,
- amount,
- calculateOrderRatio(isCkb2Udt, tipHeader),
- ),
+ isCkb2Udt ? { ckbValue: amount, udtValue: 0n } : { ckbValue: 0n, udtValue: amount },
+ system,
+ ).convertedAmount,
);
}
diff --git a/apps/interface/src/main.tsx b/apps/interface/src/main.tsx
index 9f57bc7..b31d7eb 100644
--- a/apps/interface/src/main.tsx
+++ b/apps/interface/src/main.tsx
@@ -1,58 +1,54 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import Connector from "./Connector.tsx";
-import { getIckbScriptConfigs } from "@ickb/v1-core";
-import { chainConfigFrom } from "@ickb/lumos-utils";
-import { prefetchData } from "./queries.ts";
import { ccc, JoyId } from "@ckb-ccc/ccc";
+import { getConfig, IckbSdk } from "@ickb/sdk";
+import Connector from "./Connector.tsx";
+import type { RootConfig } from "./utils.ts";
import appIcon from "/favicon.png?url";
+
const appName = "iCKB DApp";
-const testnetRootConfigPromise = chainConfigFrom(
- "testnet",
- "https://testnet.ckb.dev/",
- true,
- getIckbScriptConfigs,
-).then((chainConfig) => {
- const rootConfig = {
- ...chainConfig,
- queryClient: new QueryClient(),
- cccClient: new ccc.ClientPublicTestnet(),
- };
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- prefetchData(rootConfig);
- return rootConfig;
-});
+function createRootConfig(chain: "mainnet" | "testnet"): RootConfig {
+ const { managers, bots } = getConfig(chain);
-const mainnetRootConfigPromise = chainConfigFrom(
- "mainnet",
- "https://mainnet.ckb.dev/",
- true,
- getIckbScriptConfigs,
-).then((chainConfig) => {
- const rootConfig = {
- ...chainConfig,
+ return {
+ chain,
queryClient: new QueryClient(),
- cccClient: new ccc.ClientPublicMainnet(),
+ cccClient:
+ chain === "mainnet"
+ ? new ccc.ClientPublicMainnet()
+ : new ccc.ClientPublicTestnet(),
+ sdk: new IckbSdk(
+ managers.ownedOwner,
+ managers.logic,
+ managers.order,
+ bots,
+ ),
+ managers: {
+ ickbUdt: managers.ickbUdt,
+ logic: managers.logic,
+ ownedOwner: managers.ownedOwner,
+ order: managers.order,
+ },
};
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- prefetchData(rootConfig);
- return rootConfig;
-});
+}
+
+const rootConfigs = {
+ mainnet: createRootConfig("mainnet"),
+ testnet: createRootConfig("testnet"),
+};
-export async function startApp(wallet_chain: string): Promise {
- const [walletName, chain] = wallet_chain.split("_");
- const rootConfig = await (chain === "mainnet"
- ? mainnetRootConfigPromise
- : testnetRootConfigPromise);
+export function startApp(walletChain: string): void {
+ const [walletName, chain] = walletChain.split("_");
+ const rootConfig = chain === "mainnet" ? rootConfigs.mainnet : rootConfigs.testnet;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const signer = JoyId.getJoyIdSigners(
rootConfig.cccClient,
appName,
["https://ickb.org", appIcon].join(""),
- ).find((i) => i.name === "CKB")!.signer;
+ ).find((candidate) => candidate.name === "CKB")!.signer;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const rootElement = document.getElementById("app")!;
diff --git a/apps/interface/src/queries.ts b/apps/interface/src/queries.ts
index 8e24fec..c60cb90 100644
--- a/apps/interface/src/queries.ts
+++ b/apps/interface/src/queries.ts
@@ -1,40 +1,13 @@
+import { ccc } from "@ckb-ccc/ccc";
+import type { WithdrawalGroup } from "@ickb/core";
+import { type OrderGroup } from "@ickb/order";
+import { type SystemState } from "@ickb/sdk";
+import { collect, sum } from "@ickb/utils";
import {
- encodeToAddress,
- type TransactionSkeletonType,
-} from "@ckb-lumos/helpers";
-import { queryOptions } from "@tanstack/react-query";
-import {
- CKB,
- I8Header,
- calculateTxFee,
- capacitySifter,
- ckbDelta,
- hex,
- i8ScriptPadding,
- lockExpanderFrom,
- maturityDiscriminator,
- max,
- shuffle,
- since,
- txSize,
-} from "@ickb/lumos-utils";
-import {
- ickbDelta,
- ickbLogicScript,
- ickbPoolSifter,
- ickbSifter,
- limitOrderScript,
- orderSifter,
- ownedOwnerScript,
-} from "@ickb/v1-core";
-import {
- txInfoPadding,
- type RootConfig,
- type TxInfo,
- type WalletConfig,
-} from "./utils.ts";
-import { addChange, base as add, convert } from "./transaction.ts";
-import type { Cell, Header, HexNumber, Transaction } from "@ckb-lumos/base";
+ buildTransactionPreview,
+ type TransactionContext,
+} from "./transaction.ts";
+import { type TxInfo, type WalletConfig } from "./utils.ts";
export interface L1StateType {
ckbNative: bigint;
@@ -43,201 +16,130 @@ export interface L1StateType {
ickbBalance: bigint;
ckbAvailable: bigint;
ickbAvailable: bigint;
- tipHeader: Readonly;
- txBuilder: (isCkb2Udt: boolean, amount: bigint) => TxInfo;
+ tipTimestamp: bigint;
+ system: SystemState;
+ stateId: string;
+ txBuilder: (isCkb2Udt: boolean, amount: bigint) => Promise;
hasMatchable: boolean;
}
+export function l1StateQueryKey(
+ walletConfig: WalletConfig,
+): readonly [WalletConfig["chain"], string, "l1State"] {
+ return [walletConfig.chain, walletConfig.address, "l1State"] as const;
+}
+
export function l1StateOptions(
walletConfig: WalletConfig,
isFrozen: boolean,
-): ReturnType<
- typeof queryOptions
-> {
- return queryOptions({
- retry: true,
+): {
+ retry: number;
+ refetchInterval: (context: { state: { data?: L1StateType } }) => number;
+ staleTime: number;
+ queryKey: readonly [WalletConfig["chain"], string, "l1State"];
+ queryFn: () => Promise;
+ enabled: boolean;
+} {
+ return {
+ retry: 2,
refetchInterval: ({ state }) => 60000 * (state.data?.hasMatchable ? 1 : 10),
staleTime: 10000,
- queryKey: [walletConfig.chain, walletConfig.address, "l1State"],
- queryFn: async (): Promise => {
- return getL1State(walletConfig).catch((e: unknown) => {
- console.log(e);
- throw e;
- });
- },
- placeholderData: {
- ckbNative: 6n * CKB * CKB,
- ickbNative: 3n * CKB * CKB,
- ckbAvailable: 6n * CKB * CKB,
- ickbAvailable: 3n * CKB * CKB,
- ckbBalance: 6n * CKB * CKB,
- ickbBalance: 3n * CKB * CKB,
- tipHeader: headerPlaceholder,
- txBuilder: () => txInfoPadding,
- hasMatchable: false,
- },
+ queryKey: l1StateQueryKey(walletConfig),
+ queryFn: async () => await getL1State(walletConfig),
enabled: !isFrozen,
- });
-}
-
-async function getL1State(walletConfig: WalletConfig): Promise {
- const { rpc, config, expander, getTxSizeOverhead } = walletConfig;
-
- const mixedCells = await getMixedCells(walletConfig);
-
- // Prefetch feeRate and tipHeader
- const feeRatePromise = rpc.getFeeRate(61n);
- const tipHeaderPromise = rpc.getTipHeader();
-
- // Prefetch headers
- const wanted = new Set();
- const deferredGetHeader = (blockNumber: string): Readonly => {
- wanted.add(blockNumber);
- return headerPlaceholder;
- };
- ickbSifter(mixedCells, expander, deferredGetHeader, config);
- const headersPromise = getHeadersByNumber(wanted, walletConfig);
-
- // Prefetch txs outputs
- const wantedTxsOutputs = new Set();
- const deferredGetTxsOutputs = (txHash: string): never[] => {
- wantedTxsOutputs.add(txHash);
- return [];
};
- orderSifter(mixedCells, expander, deferredGetTxsOutputs, config);
- const txsOutputsPromise = getTxsOutputs(wantedTxsOutputs, walletConfig);
-
- // Do potentially costly operations
- const { capacities, notCapacities } = capacitySifter(mixedCells, expander);
-
- // Await for headers
- const headers = await headersPromise;
+}
- // Sift through iCKB related cells
- const {
- udts,
- receipts,
- withdrawalRequestGroups,
- ickbPool: pool,
- notIckbs,
- } = ickbSifter(
- notCapacities,
- expander,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (blockNumber) => headers.get(blockNumber)!,
- config,
+export async function getL1State(
+ walletConfig: WalletConfig,
+): Promise {
+ const sdkState = await walletConfig.sdk.getL1State(
+ walletConfig.cccClient,
+ walletConfig.accountLocks,
);
+ const { system, user } = sdkState;
+ const [accountCells, receipts, withdrawalGroups] = await Promise.all([
+ getAccountCells(walletConfig),
+ collect(
+ walletConfig.managers.logic.findReceipts(
+ walletConfig.cccClient,
+ walletConfig.accountLocks,
+ { onChain: true },
+ ),
+ ),
+ collect(
+ walletConfig.managers.ownedOwner.findWithdrawalGroups(
+ walletConfig.cccClient,
+ walletConfig.accountLocks,
+ { onChain: true, tip: system.tip },
+ ),
+ ),
+ ]);
- const tipHeader = I8Header.from(await tipHeaderPromise);
- // Partition between ripe and non ripe withdrawal requests
- const { mature, notMature } = maturityDiscriminator(
- withdrawalRequestGroups,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (g) => g.ownedWithdrawalRequest.cellOutput.type![since],
- tipHeader,
+ const capacityCells = accountCells.filter((cell) => cell.cellOutput.type === undefined);
+ const udtCells = accountCells.filter((cell) =>
+ walletConfig.managers.ickbUdt.isUdt(cell),
);
-
- // min lock: 1/4 epoch (~ 1 hour)
- const minLock = { length: 4, index: 1, number: 0 };
- // Sort the ickbPool based on the tip header
- let ickbPool = ickbPoolSifter(pool, tipHeader, minLock);
- // Take a random convenient subset of max 40 deposits
- if (ickbPool.length > 40) {
- const n = max(Math.round(ickbPool.length / 180), 40);
- ickbPool = shuffle(ickbPool.slice(0, n).map((d, i) => ({ d, i })))
- .slice(0, 40)
- .sort((a, b) => a.i - b.i)
- .map((a) => a.d);
- }
-
- // Await for txsOutputs
- const txsOutputs = await txsOutputsPromise;
-
- // Sift through Orders
- const { myOrders } = orderSifter(
- notIckbs,
- expander,
- (txHash) => txsOutputs.get(txHash) ?? [],
- config,
+ const nativeUdtInfo = await walletConfig.managers.ickbUdt.infoFrom(
+ walletConfig.cccClient,
+ udtCells,
);
- const matchableOrders = [];
- const completedOrders = [];
- for (const o of myOrders) {
- const { isMatchable, isDualRatio } = o.info;
- if (isMatchable && !isDualRatio) {
- matchableOrders.push(o);
+ const ckbNative =
+ sum(0n, ...capacityCells.map((cell) => cell.cellOutput.capacity)) +
+ nativeUdtInfo.capacity;
+ const ickbNative = nativeUdtInfo.balance;
+
+ const readyWithdrawals: WithdrawalGroup[] = [];
+ const pendingWithdrawals: WithdrawalGroup[] = [];
+ for (const group of withdrawalGroups) {
+ if (group.owned.isReady) {
+ readyWithdrawals.push(group);
} else {
- // Withdraw completed orders and dual ratio orders
- completedOrders.push(o);
+ pendingWithdrawals.push(group);
}
}
- const hasMatchable = matchableOrders.length > 0;
-
- const txHasNonNative =
- mature.length > 0 || receipts.length > 0 || completedOrders.length > 0;
-
- // Calculate native balances
- let txInfo = add({ capacities, udts, tipHeader });
- const ckbNative = ckbDelta(txInfo.tx, config);
- const ickbNative = ickbDelta(txInfo.tx, config);
-
- // Calculate Available balances and baseTx
- txInfo = add({
- txInfo,
- myOrders: completedOrders,
- receipts,
- wrGroups: mature,
- tipHeader,
- });
- const txSizeOverheadPromise = getTxSizeOverhead(txInfo.tx);
- const ckbAvailable = ckbDelta(txInfo.tx, config);
- const ickbAvailable = ickbDelta(txInfo.tx, config);
-
- // Calculate full balances
- const fullTxInfo = add({
- txInfo,
- myOrders: matchableOrders,
- wrGroups: notMature,
- tipHeader,
- });
- const ckbBalance = ckbDelta(fullTxInfo.tx, config);
- const ickbBalance = ickbDelta(fullTxInfo.tx, config);
- txInfo = Object.freeze({
- ...txInfo,
- estimatedMaturity: fullTxInfo.estimatedMaturity,
- });
-
- const [txSizeOverhead, feeRate] = await Promise.all([
- txSizeOverheadPromise,
- feeRatePromise,
- ]);
-
- const calculateFee = (tx: TransactionSkeletonType): bigint =>
- calculateTxFee(txSize(tx) + txSizeOverhead, feeRate);
-
- const txBuilder = (isCkb2Udt: boolean, amount: bigint): TxInfo => {
- if (amount > 0n) {
- return convert(
- txInfo,
- isCkb2Udt,
- amount,
- ickbPool,
- tipHeader,
- calculateFee,
- walletConfig,
- );
- }
-
- if (txHasNonNative) {
- return addChange(txInfo, calculateFee, walletConfig);
+ const availableOrders: OrderGroup[] = [];
+ const pendingOrders: OrderGroup[] = [];
+ for (const group of user.orders) {
+ if (group.order.isDualRatio() || !group.order.isMatchable()) {
+ availableOrders.push(group);
+ } else {
+ pendingOrders.push(group);
}
+ }
- return Object.freeze({
- ...txInfo,
- error: "Nothing to do for now",
- });
+ const ckbAvailable =
+ ckbNative +
+ sumCkb(receipts) +
+ sumCkb(readyWithdrawals) +
+ sumCkb(availableOrders);
+ const ickbAvailable =
+ ickbNative +
+ sumUdt(receipts) +
+ sumUdt(readyWithdrawals) +
+ sumUdt(availableOrders);
+
+ const ckbBalance = ckbAvailable + sumCkb(pendingWithdrawals) + sumCkb(pendingOrders);
+ const ickbBalance = ickbAvailable + sumUdt(pendingWithdrawals) + sumUdt(pendingOrders);
+
+ const estimatedMaturity = [
+ system.tip.timestamp,
+ ...pendingWithdrawals.map((group) => group.owned.maturity.toUnix(system.tip)),
+ ...pendingOrders
+ .map((group) => group.order.maturity)
+ .filter((maturity): maturity is bigint => maturity !== undefined),
+ ].reduce((best, maturity) => (best > maturity ? best : maturity));
+
+ const txContext: TransactionContext = {
+ system,
+ receipts,
+ readyWithdrawals,
+ availableOrders,
+ ckbAvailable,
+ ickbAvailable,
+ estimatedMaturity,
};
return {
@@ -247,149 +149,52 @@ async function getL1State(walletConfig: WalletConfig): Promise {
ickbBalance,
ckbAvailable,
ickbAvailable,
- tipHeader,
- txBuilder,
- hasMatchable,
- };
-}
-
-export async function prefetchData(rootConfig: RootConfig): Promise {
- const { queryClient } = rootConfig;
- const dummy: WalletConfig = {
- ...rootConfig,
- accountLocks: [i8ScriptPadding],
- address: encodeToAddress(i8ScriptPadding, rootConfig),
- expander: lockExpanderFrom(i8ScriptPadding),
- getTxSizeOverhead: () => Promise.resolve(0),
- sendSigned: () => Promise.resolve("0x0"),
+ tipTimestamp: system.tip.timestamp,
+ system,
+ stateId: [
+ walletConfig.chain,
+ String(system.tip.timestamp),
+ String(receipts.length),
+ String(readyWithdrawals.length),
+ String(pendingWithdrawals.length),
+ String(availableOrders.length),
+ String(pendingOrders.length),
+ ].join(":"),
+ txBuilder: (isCkb2Udt, amount) =>
+ buildTransactionPreview(txContext, isCkb2Udt, amount, walletConfig),
+ hasMatchable: pendingOrders.length > 0,
};
-
- return queryClient.prefetchQuery(l1StateOptions(dummy, false));
}
-async function getMixedCells(
- walletConfig: WalletConfig,
-): Promise {
- const { accountLocks, config, rpc } = walletConfig;
-
- return Object.freeze(
- (
- await Promise.all(
- [
- ...accountLocks,
- ickbLogicScript(config),
- ownedOwnerScript(config),
- limitOrderScript(config),
- ].map((lock) => rpc.getCellsByLock(lock, "desc", "max")),
- )
- ).flat(),
- );
-}
-
-async function getTxsOutputs(
- txHashes: Set,
- walletConfig: WalletConfig,
-): Promise>> {
- const { chain, rpc, queryClient } = walletConfig;
-
- const known: Readonly