Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 32 additions & 39 deletions apps/interface/src/Action.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { useState, type JSX } from "react";
import type { L1StateType } from "./queries.ts";
import { sendAndWaitForCommit } from "@ickb/sdk";
import { getL1State, type L1StateType } from "./queries.ts";
import Progress from "./Progress.tsx";
import {
errorMessageOf,
Expand All @@ -19,7 +20,6 @@ export default function Action({
walletConfig,
l1State,
isStateFetching,
isStateStale,
}: {
isCkb2Udt: boolean;
amount: bigint;
Expand All @@ -28,7 +28,6 @@ export default function Action({
walletConfig: WalletConfig;
l1State: L1StateType | undefined;
isStateFetching: boolean;
isStateStale: boolean;
}): JSX.Element {
const [message, setMessage] = useState("");
const [failure, setFailure] = useState("");
Expand Down Expand Up @@ -79,14 +78,10 @@ export default function Action({
<button
className="text-s col-span-2 min-h-12 w-full cursor-pointer rounded border-2 border-amber-400 px-8 leading-relaxed font-bold tracking-wider text-amber-400 uppercase disabled:cursor-default disabled:opacity-50"
onClick={() => {
if (isStateStale) {
void walletConfig.queryClient.invalidateQueries({
queryKey: [walletConfig.chain, walletConfig.address, "l1State"],
});
return;
}

void transact(
() => getL1State(walletConfig).then((freshState) =>
freshState.txBuilder(isCkb2Udt, amount)
),
txInfo,
freezeTxInfo,
setMessage,
Expand All @@ -105,11 +100,9 @@ export default function Action({
? txInfo.error
: !isValid
? "nothing to do right now"
: isStateStale
? `refresh before ${amount > 0n ? `converting to ${isCkb2Udt ? "iCKB" : "CKB"}` : "collecting converted funds"}`
: amount > 0n
? `request conversion to ${isCkb2Udt ? "iCKB" : "CKB"}`
: `${isReady ? "fully" : "partially"} collect converted funds`}
: amount > 0n
? `request conversion to ${isCkb2Udt ? "iCKB" : "CKB"}`
: `${isReady ? "fully" : "partially"} collect converted funds`}
</button>
</Progress>
{failure !== "" ? <span className="col-span-2 text-center text-red-400">{failure}</span> : null}
Expand All @@ -122,50 +115,50 @@ export default function Action({
}

async function transact(
txInfo: TxInfo,
buildFreshTxInfo: () => Promise<TxInfo>,
previewTxInfo: TxInfo,
freezeTxInfo: (txInfo: TxInfo) => void,
setMessage: (message: string) => void,
setFailure: (message: string) => void,
formReset: () => void,
walletConfig: WalletConfig,
): Promise<void> {
const { address, chain, cccClient, queryClient, signer } = walletConfig;
const maxConfirmationChecks = 60;
const { address, chain, queryClient } = walletConfig;

try {
freezeTxInfo(txInfo);
freezeTxInfo(previewTxInfo);
setFailure("");
setMessage("Waiting for user confirmation...");
const txHash = await signer.sendTransaction(txInfo.tx);

let status: string | undefined = "sent";
let checks = 0;
while (
checks < maxConfirmationChecks &&
(status === undefined || ["sent", "pending", "proposed"].includes(status))
) {
setMessage("Waiting for network confirmation...");
await new Promise((resolve) => setTimeout(resolve, 10000));
status = (await cccClient.getTransaction(txHash))?.status;
checks += 1;
setMessage("Refreshing transaction...");
const txInfo = await buildFreshTxInfo();
if (txInfo.error !== "") {
throw new Error(txInfo.error);
}

if (checks >= maxConfirmationChecks) {
throw new Error("Transaction confirmation timed out");
if (!hasTransactionActivity(txInfo.tx)) {
throw new Error("Nothing to do right now");
}

if (status !== "committed") {
throw new Error(`Transaction ended with status: ${status ?? "unknown"}`);
if (txInfo.fee <= 0n) {
throw new Error("Transaction fee is missing or invalid");
}

freezeTxInfo(txInfo);
setMessage("Waiting for user confirmation...");
await sendAndWaitForCommit({
client: walletConfig.cccClient,
signer: walletConfig.signer,
}, txInfo.tx, {
onConfirmationWait: () => {
setMessage("Waiting for network confirmation...");
},
});

setMessage("Transaction confirmed.");
formReset();
await new Promise((resolve) => setTimeout(resolve, 2000));
} catch (error) {
setFailure(errorMessageOf(error));
} finally {
await queryClient.invalidateQueries({ queryKey: [chain, address] });
freezeTxInfo(txInfoPadding);
await queryClient.invalidateQueries({ queryKey: [chain, address] });
Comment thread
phroi marked this conversation as resolved.
setMessage("");
}
}
Expand Down
12 changes: 2 additions & 10 deletions apps/interface/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { Dashboard } from "./Dashboard.tsx";
import Form from "./Form.tsx";
import Progress from "./Progress.tsx";
import {
getL1State,
l1StateQueryKey,
l1StateOptions,
type L1StateType,
} from "./queries.ts";
import {
Expand All @@ -25,12 +24,7 @@ export default function App({
const [isFrozen, freeze] = useState(false);
const [rawText, setRawText] = useState(direction2Symbol(true));
const l1StateQuery = useQuery<L1StateType>({
retry: 2,
refetchInterval: ({ state }) => 60000 * (state.data?.hasMatchable ? 1 : 10),
staleTime: 10000,
queryKey: l1StateQueryKey(walletConfig),
queryFn: async () => await getL1State(walletConfig),
enabled: !isFrozen,
...l1StateOptions(walletConfig, isFrozen),
});
const symbol = rawText.startsWith("I") ? "I" : "C";
const isCkb2Udt = symbol2Direction(symbol);
Expand All @@ -46,7 +40,6 @@ export default function App({
walletConfig: WalletConfig;
l1State: L1StateType | undefined;
isStateFetching: boolean;
isStateStale: boolean;
}>({
isCkb2Udt,
amount,
Expand All @@ -55,7 +48,6 @@ export default function App({
walletConfig,
l1State: l1StateQuery.data,
isStateFetching: l1StateQuery.isFetching,
isStateStale: l1StateQuery.isStale,
});

if (l1StateQuery.data === undefined) {
Expand Down
124 changes: 94 additions & 30 deletions apps/interface/src/queries.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ccc } from "@ckb-ccc/ccc";
import { Ratio, type OrderGroup } from "@ickb/order";
import { describe, expect, it } from "vitest";
import { isPlainCapacityCell } from "@ickb/utils";
import { getL1State } from "./queries.ts";
import type { WalletConfig } from "./utils.ts";

function byte32FromByte(hexByte: string): `0x${string}` {
if (!/^[0-9a-f]{2}$/iu.test(hexByte)) {
Expand All @@ -17,36 +19,98 @@ function script(codeHashByte: string): ccc.Script {
});
}

describe("isPlainCapacityCell", () => {
it("accepts only no-type empty-data cells", () => {
const plain = ccc.Cell.from({
outPoint: { txHash: byte32FromByte("11"), index: 0n },
cellOutput: {
capacity: ccc.fixedPointFrom(1000),
lock: script("22"),
},
outputData: "0x",
});
const typed = ccc.Cell.from({
outPoint: { txHash: byte32FromByte("33"), index: 0n },
cellOutput: {
capacity: ccc.fixedPointFrom(2000),
lock: script("22"),
type: script("44"),
},
outputData: "0x",
});
const dataCell = ccc.Cell.from({
outPoint: { txHash: byte32FromByte("55"), index: 0n },
cellOutput: {
capacity: ccc.fixedPointFrom(3000),
lock: script("22"),
function cell(capacity: bigint, lock: ccc.Script): ccc.Cell {
return ccc.Cell.from({
outPoint: { txHash: byte32FromByte("aa"), index: 0n },
cellOutput: { capacity, lock },
outputData: "0x",
});
}

function orderGroup(
ckbValue: bigint,
udtValue: bigint,
isMatchable: boolean,
maturity?: bigint,
): OrderGroup {
return {
ckbValue,
udtValue,
order: {
isDualRatio: (): boolean => false,
isMatchable: (): boolean => isMatchable,
maturity,
},
} as OrderGroup;
}

describe("getL1State", () => {
it("projects account state through the SDK and makes collected orders available", async () => {
const lock = script("11");
const tip = { timestamp: 10n } as ccc.ClientBlockHeader;
const nativeCapacity = ccc.fixedPointFrom(50);
const receipt = { ckbValue: 13n, udtValue: 17n };
const readyWithdrawal = {
ckbValue: 19n,
udtValue: 0n,
owned: { isReady: true },
};
const pendingWithdrawal = {
ckbValue: 31n,
udtValue: 0n,
owned: {
isReady: false,
maturity: { toUnix: (): bigint => 60n },
},
outputData: "0xab",
});
};
const availableOrder = orderGroup(10n, 20n, false);
const pendingOrder = orderGroup(100n, 200n, true, 40n);
const walletConfig: WalletConfig = {
chain: "testnet",
cccClient: {} as ccc.Client,
queryClient: {} as WalletConfig["queryClient"],
signer: {} as ccc.Signer,
address: "ckt1test",
accountLocks: [lock],
primaryLock: lock,
sdk: {
getL1State: async () => {
await Promise.resolve();
return {
system: {
feeRate: 1n,
tip,
exchangeRatio: Ratio.from({ ckbScale: 1n, udtScale: 1n }),
orderPool: [],
ckbAvailable: 0n,
ckbMaturing: [],
},
user: { orders: [availableOrder, pendingOrder] },
};
},
getAccountState: async () => {
await Promise.resolve();
return {
capacityCells: [cell(nativeCapacity, lock)],
nativeUdtCapacity: 7n,
nativeUdtBalance: 11n,
receipts: [receipt],
withdrawalGroups: [readyWithdrawal, pendingWithdrawal],
};
},
} as unknown as WalletConfig["sdk"],
managers: {} as WalletConfig["managers"],
};

const state = await getL1State(walletConfig);

expect(isPlainCapacityCell(plain)).toBe(true);
expect(isPlainCapacityCell(typed)).toBe(false);
expect(isPlainCapacityCell(dataCell)).toBe(false);
expect(state.ckbNative).toBe(nativeCapacity);
expect(state.ickbNative).toBe(11n);
expect(state.ckbAvailable).toBe(nativeCapacity + 142n);
expect(state.ickbAvailable).toBe(248n);
expect(state.ckbBalance).toBe(nativeCapacity + 173n);
expect(state.ickbBalance).toBe(248n);
expect(state.hasMatchable).toBe(false);
expect(state.stateId).toBe("testnet:10:1:1:1:2:0");
});
});
Loading
Loading