@ickb/sdk 1000.0.82 → 9001.0.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/README.md +84 -32
- package/dist/constants.d.ts +9 -22
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +62 -76
- package/dist/constants.js.map +1 -1
- package/dist/conversion/estimate.d.ts +33 -0
- package/dist/conversion/estimate.d.ts.map +1 -0
- package/dist/conversion/estimate.js +117 -0
- package/dist/conversion/estimate.js.map +1 -0
- package/dist/conversion/maturity.d.ts +15 -0
- package/dist/conversion/maturity.d.ts.map +1 -0
- package/dist/conversion/maturity.js +113 -0
- package/dist/conversion/maturity.js.map +1 -0
- package/dist/conversion/plans.d.ts +20 -0
- package/dist/conversion/plans.d.ts.map +1 -0
- package/dist/conversion/plans.js +106 -0
- package/dist/conversion/plans.js.map +1 -0
- package/dist/conversion/projection.d.ts +21 -0
- package/dist/conversion/projection.d.ts.map +1 -0
- package/dist/conversion/projection.js +94 -0
- package/dist/conversion/projection.js.map +1 -0
- package/dist/conversion/types.d.ts +264 -0
- package/dist/conversion/types.d.ts.map +1 -0
- package/dist/conversion/types.js +2 -0
- package/dist/conversion/types.js.map +1 -0
- package/dist/conversion/withdrawal_ring.d.ts +34 -0
- package/dist/conversion/withdrawal_ring.d.ts.map +1 -0
- package/dist/conversion/withdrawal_ring.js +87 -0
- package/dist/conversion/withdrawal_ring.js.map +1 -0
- package/dist/dao.d.ts +100 -0
- package/dist/dao.d.ts.map +1 -0
- package/dist/dao.js +125 -0
- package/dist/dao.js.map +1 -0
- package/dist/index.d.ts +49 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -3
- package/dist/index.js.map +1 -1
- package/dist/logic.d.ts +81 -0
- package/dist/logic.d.ts.map +1 -0
- package/dist/logic.js +173 -0
- package/dist/logic.js.map +1 -0
- package/dist/order/cells.d.ts +168 -0
- package/dist/order/cells.d.ts.map +1 -0
- package/dist/order/cells.js +309 -0
- package/dist/order/cells.js.map +1 -0
- package/dist/order/conversion.d.ts +37 -0
- package/dist/order/conversion.d.ts.map +1 -0
- package/dist/order/conversion.js +120 -0
- package/dist/order/conversion.js.map +1 -0
- package/dist/order/fee.d.ts +9 -0
- package/dist/order/fee.d.ts.map +1 -0
- package/dist/order/fee.js +24 -0
- package/dist/order/fee.js.map +1 -0
- package/dist/order/fill.d.ts +36 -0
- package/dist/order/fill.d.ts.map +1 -0
- package/dist/order/fill.js +53 -0
- package/dist/order/fill.js.map +1 -0
- package/dist/order/info.d.ts +62 -0
- package/dist/order/info.d.ts.map +1 -0
- package/dist/order/info.js +102 -0
- package/dist/order/info.js.map +1 -0
- package/dist/order/master.d.ts +49 -0
- package/dist/order/master.d.ts.map +1 -0
- package/dist/order/master.js +27 -0
- package/dist/order/master.js.map +1 -0
- package/dist/order/matcher.d.ts +43 -0
- package/dist/order/matcher.d.ts.map +1 -0
- package/dist/order/matcher.js +124 -0
- package/dist/order/matcher.js.map +1 -0
- package/dist/order/order.d.ts +63 -0
- package/dist/order/order.d.ts.map +1 -0
- package/dist/order/order.js +234 -0
- package/dist/order/order.js.map +1 -0
- package/dist/order/order_data.d.ts +51 -0
- package/dist/order/order_data.d.ts.map +1 -0
- package/dist/order/order_data.js +67 -0
- package/dist/order/order_data.js.map +1 -0
- package/dist/order/ratio.d.ts +40 -0
- package/dist/order/ratio.d.ts.map +1 -0
- package/dist/order/ratio.js +74 -0
- package/dist/order/ratio.js.map +1 -0
- package/dist/order/relative.d.ts +43 -0
- package/dist/order/relative.d.ts.map +1 -0
- package/dist/order/relative.js +57 -0
- package/dist/order/relative.js.map +1 -0
- package/dist/owned_owner.d.ts +108 -0
- package/dist/owned_owner.d.ts.map +1 -0
- package/dist/owned_owner.js +260 -0
- package/dist/owned_owner.js.map +1 -0
- package/dist/sdk.d.ts +113 -41
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +410 -209
- package/dist/sdk.js.map +1 -1
- package/dist/send/sign_and_send_transaction.d.ts +61 -0
- package/dist/send/sign_and_send_transaction.d.ts.map +1 -0
- package/dist/send/sign_and_send_transaction.js +201 -0
- package/dist/send/sign_and_send_transaction.js.map +1 -0
- package/dist/send/wait_transaction.d.ts +42 -0
- package/dist/send/wait_transaction.d.ts.map +1 -0
- package/dist/send/wait_transaction.js +172 -0
- package/dist/send/wait_transaction.js.map +1 -0
- package/dist/udt.d.ts +108 -0
- package/dist/udt.d.ts.map +1 -0
- package/dist/udt.js +180 -0
- package/dist/udt.js.map +1 -0
- package/dist/utils/chain.d.ts +3 -0
- package/dist/utils/chain.d.ts.map +1 -0
- package/dist/utils/chain.js +2 -0
- package/dist/utils/chain.js.map +1 -0
- package/dist/utils/codec.d.ts +19 -0
- package/dist/utils/codec.d.ts.map +1 -0
- package/dist/utils/codec.js +69 -0
- package/dist/utils/codec.js.map +1 -0
- package/dist/utils/utils.d.ts +99 -0
- package/dist/utils/utils.d.ts.map +1 -0
- package/dist/utils/utils.js +130 -0
- package/dist/utils/utils.js.map +1 -0
- package/dist/utils/verified_client.d.ts +19 -0
- package/dist/utils/verified_client.d.ts.map +1 -0
- package/dist/utils/verified_client.js +92 -0
- package/dist/utils/verified_client.js.map +1 -0
- package/package.json +19 -26
- package/src/constants.ts +65 -181
- package/src/conversion/estimate.ts +155 -0
- package/src/conversion/maturity.ts +158 -0
- package/src/conversion/plans.ts +144 -0
- package/src/conversion/projection.ts +136 -0
- package/src/conversion/types.ts +302 -0
- package/src/conversion/withdrawal_ring.ts +122 -0
- package/src/dao.ts +196 -0
- package/src/index.ts +89 -3
- package/src/logic.ts +283 -0
- package/src/order/cells.ts +383 -0
- package/src/order/conversion.ts +162 -0
- package/src/order/fee.ts +30 -0
- package/src/order/fill.ts +83 -0
- package/src/order/info.ts +181 -0
- package/src/order/master.ts +42 -0
- package/src/order/matcher.ts +191 -0
- package/src/order/order.ts +288 -0
- package/src/order/order_data.ts +131 -0
- package/src/order/ratio.ts +127 -0
- package/src/order/relative.ts +109 -0
- package/src/owned_owner.ts +364 -0
- package/src/sdk.ts +542 -440
- package/src/send/sign_and_send_transaction.ts +256 -0
- package/src/send/wait_transaction.ts +227 -0
- package/src/udt.ts +249 -0
- package/src/utils/chain.ts +2 -0
- package/src/utils/codec.ts +78 -0
- package/src/utils/utils.ts +208 -0
- package/src/utils/verified_client.ts +136 -0
- package/dist/codec.d.ts +0 -3
- package/dist/codec.d.ts.map +0 -1
- package/dist/codec.js +0 -58
- package/dist/codec.js.map +0 -1
- package/src/codec.ts +0 -138
package/src/sdk.ts
CHANGED
|
@@ -1,513 +1,615 @@
|
|
|
1
1
|
import { ccc } from "@ckb-ccc/core";
|
|
2
|
+
import { getConfig } from "./constants.ts";
|
|
3
|
+
import { minimumOrderAmount } from "./conversion/estimate.ts";
|
|
2
4
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
ckbToIckbConversionPlans,
|
|
6
|
+
ickbToCkbConversionPlans,
|
|
7
|
+
} from "./conversion/plans.ts";
|
|
8
|
+
import type {
|
|
9
|
+
AccountState,
|
|
10
|
+
CompleteIckbTransactionOptions,
|
|
11
|
+
ConversionTransactionContext,
|
|
12
|
+
ConversionTransactionFailureReason,
|
|
13
|
+
ConversionTransactionOptions,
|
|
14
|
+
ConversionTransactionResult,
|
|
15
|
+
SdkManagers,
|
|
16
|
+
SystemState,
|
|
17
|
+
} from "./conversion/types.ts";
|
|
12
18
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} from "
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
19
|
+
assertDaoOutputLimit,
|
|
20
|
+
assertFresh,
|
|
21
|
+
broadcastDeadline,
|
|
22
|
+
DaoHeaderIndexError,
|
|
23
|
+
DaoOutputLimitError,
|
|
24
|
+
type LockUpPolicy,
|
|
25
|
+
pushHeaderDep,
|
|
26
|
+
tipMaxAgeMs,
|
|
27
|
+
} from "./dao.ts";
|
|
28
|
+
import type { IckbDepositCell, LogicManager } from "./logic.ts";
|
|
29
|
+
import type { OrderGroup } from "./order/cells.ts";
|
|
30
|
+
import type { OrderManager } from "./order/order.ts";
|
|
31
|
+
import { Ratio } from "./order/ratio.ts";
|
|
32
|
+
import type { OwnedOwnerManager } from "./owned_owner.ts";
|
|
33
|
+
import { ickbExchangeRatio, type IckbUdt, type ProtocolCell } from "./udt.ts";
|
|
34
|
+
import type { SupportedChain } from "./utils/chain.ts";
|
|
35
|
+
import { compareBigInt, findCells, isPlainCapacityCell, unique } from "./utils/utils.ts";
|
|
36
|
+
import { verifiedClient } from "./utils/verified_client.ts";
|
|
28
37
|
|
|
29
38
|
/**
|
|
30
|
-
*
|
|
39
|
+
* Input count up to which one own transaction sweeps liquid cells.
|
|
40
|
+
*
|
|
41
|
+
* @remarks A compaction target, not a validity bound: required funding and the fee may go
|
|
42
|
+
* past it. Counted rather than measured in bytes because a same-lock input costs a fixed
|
|
43
|
+
* 44 bytes (52 with an extra witness slot), so 1,000 inputs stay near 50 KB, far under the
|
|
44
|
+
* node's 512,000-byte limit, and near the one measured point (1,000 inputs: 44 KB, about
|
|
45
|
+
* 180 ms of local completion). A miner paying the bot adds about forty cellbase cells per
|
|
46
|
+
* turn, which this outpaces; a larger backlog takes several transactions.
|
|
31
47
|
*/
|
|
32
|
-
export
|
|
33
|
-
/**
|
|
34
|
-
* Creates an instance of IckbSdk.
|
|
35
|
-
*
|
|
36
|
-
* @param ownedOwner - The manager for owned owner operations.
|
|
37
|
-
* @param ickbLogic - The manager for iCKB logic operations.
|
|
38
|
-
* @param order - The manager for order operations.
|
|
39
|
-
* @param capacity - The capacity manager instance.
|
|
40
|
-
* @param bots - An array of bot lock scripts.
|
|
41
|
-
*/
|
|
42
|
-
constructor(
|
|
43
|
-
private readonly ownedOwner: OwnedOwnerManager,
|
|
44
|
-
private readonly ickbLogic: LogicManager,
|
|
45
|
-
private readonly order: OrderManager,
|
|
46
|
-
private readonly capacity: CapacityManager,
|
|
47
|
-
private readonly bots: ccc.Script[],
|
|
48
|
-
) {}
|
|
48
|
+
export const SWEEP_INPUT_LIMIT = 1000;
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*
|
|
53
|
-
* @param args - Parameters matching those of getConfig.
|
|
54
|
-
* @returns A new instance of IckbSdk.
|
|
55
|
-
*/
|
|
56
|
-
static from(...args: Parameters<typeof getConfig>): IckbSdk {
|
|
57
|
-
const {
|
|
58
|
-
managers: { ownedOwner, logic, order, capacity },
|
|
59
|
-
bots,
|
|
60
|
-
} = getConfig(...args);
|
|
50
|
+
/** The options with the lock resolved to the signer's recommended one when absent. */
|
|
51
|
+
type ResolvedConversionOptions = ConversionTransactionOptions & { lock: ccc.Script };
|
|
61
52
|
|
|
62
|
-
|
|
53
|
+
/** What every own transaction collects along: the context's collectable positions. */
|
|
54
|
+
type CollectSteps = Pick<
|
|
55
|
+
ConversionTransactionContext,
|
|
56
|
+
"availableOrders" | "receipts" | "readyWithdrawals"
|
|
57
|
+
>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The whole SDK, which the Node actors drive by relative import; the package barrel
|
|
61
|
+
* exposes the two conversion-workflow methods, and its assignment checks they agree.
|
|
62
|
+
*/
|
|
63
|
+
export class IckbSdk {
|
|
64
|
+
private readonly ickbUdt: IckbUdt;
|
|
65
|
+
private readonly ownedOwner: OwnedOwnerManager;
|
|
66
|
+
private readonly ickbLogic: LogicManager;
|
|
67
|
+
private readonly order: OrderManager;
|
|
68
|
+
|
|
69
|
+
constructor({ ickbUdt, ownedOwner, ickbLogic, order }: SdkManagers) {
|
|
70
|
+
this.ickbUdt = ickbUdt;
|
|
71
|
+
this.ownedOwner = ownedOwner;
|
|
72
|
+
this.ickbLogic = ickbLogic;
|
|
73
|
+
this.order = order;
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
/** Creates the SDK for one chain's deployment. */
|
|
77
|
+
public static fromChain(chain: SupportedChain): IckbSdk {
|
|
78
|
+
return new IckbSdk(getConfig(chain));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- State reads -------------------------------------------------------------------
|
|
82
|
+
|
|
65
83
|
/**
|
|
66
|
-
*
|
|
84
|
+
* Reads system, user-order, and account state at the sampled tip's parent, under the
|
|
85
|
+
* caller's withdrawal timing rules (`BOT_LOCK_UP` or `WALLET_LOCK_UP`) and, when given, a
|
|
86
|
+
* positive fee rate cap that replaces CCC's ceiling; a cap of zero or less throws.
|
|
67
87
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* @param isCkb2Udt - Indicates the conversion direction:
|
|
73
|
-
* - true: Convert CKB to UDT.
|
|
74
|
-
* - false: Convert UDT to CKB.
|
|
75
|
-
* @param amounts - An object containing value components (amounts for CKB and UDT).
|
|
76
|
-
* @param system - The current system state containing exchange ratio, fee rate, tip, and order-related information.
|
|
77
|
-
* @param options - Optional parameters for fee and matching:
|
|
78
|
-
* - fee: The fee to apply in integer terms (defaults to 1n for 0.001% fee).
|
|
79
|
-
* - feeBase: The base used for fee scaling (defaults to 100000n).
|
|
80
|
-
*
|
|
81
|
-
* @returns An object with:
|
|
82
|
-
* - convertedAmount: The estimated converted amount as a FixedPoint.
|
|
83
|
-
* - ckbFee: The fee (or gain) in CKB, as a FixedPoint.
|
|
84
|
-
* - info: Additional conversion metadata.
|
|
85
|
-
* - maturity: Optional maturity information (as ccc.Num) if meets criteria.
|
|
88
|
+
* @remarks Every read goes through `verifiedClient` and the tip's parent is bounded by the
|
|
89
|
+
* policy's slack, so a caller passes a plain client. Every read is complete and uncapped: a large
|
|
90
|
+
* book or pool costs a slower read, never a partial or failed one. Each account lock is
|
|
91
|
+
* enumerated by one uncached unfiltered exact-lock scan and classified client-side.
|
|
86
92
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
93
|
+
public async getL1AccountState(
|
|
94
|
+
plainClient: ccc.Client,
|
|
95
|
+
locks: ccc.Script[],
|
|
96
|
+
lockUp: LockUpPolicy,
|
|
97
|
+
maxFeeRate?: ccc.NumLike,
|
|
98
|
+
): Promise<{
|
|
99
|
+
system: SystemState;
|
|
100
|
+
userOrders: OrderGroup[];
|
|
101
|
+
account: AccountState;
|
|
102
|
+
}> {
|
|
103
|
+
// CCC reads a zero cap as no cap at all and would plan at a negative one.
|
|
104
|
+
if (maxFeeRate !== undefined && ccc.numFrom(maxFeeRate) <= ccc.Zero) {
|
|
105
|
+
throw new RangeError("maxFeeRate must be positive");
|
|
106
|
+
}
|
|
107
|
+
const client = verifiedClient(plainClient);
|
|
108
|
+
const live = await client.getTipHeader();
|
|
109
|
+
// The live tip can still lose a one-block race, and a transaction naming it as a header
|
|
110
|
+
// dep is then refused after signing; every recorded fork was one block deep.
|
|
111
|
+
const tip = await client.getHeaderByNumber(live.number - 1n);
|
|
112
|
+
if (tip?.hash !== live.parentHash) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Block ${String(live.number - 1n)} is not the parent of tip ${live.hash}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
// The age bound is on the header the read is priced at: a fresh tip a node invented can
|
|
118
|
+
// name any old block as its parent.
|
|
119
|
+
assertFresh(tip, tipMaxAgeMs(lockUp));
|
|
120
|
+
const exchangeRatio = Ratio.from(ickbExchangeRatio(tip));
|
|
121
|
+
const [poolDeposits, orders, feeRate, accounts] = await Promise.all([
|
|
122
|
+
this.ickbLogic.findDeposits(client, tip),
|
|
123
|
+
this.order.findOrders(client),
|
|
124
|
+
// CCC clamps the sampled median at the cap, the connector's chosen rate too. A given cap
|
|
125
|
+
// replaces CCC's default ceiling, on purpose: the caller's cap is the ceiling.
|
|
126
|
+
client.getFeeRate(undefined, maxFeeRate === undefined ? undefined : { maxFeeRate }),
|
|
127
|
+
Promise.all(
|
|
128
|
+
[...unique(locks)].map(async (lock) => this.readLockCells(client, lock, tip)),
|
|
129
|
+
),
|
|
130
|
+
]);
|
|
131
|
+
return {
|
|
132
|
+
system: {
|
|
133
|
+
feeRate,
|
|
134
|
+
tip,
|
|
135
|
+
exchangeRatio,
|
|
136
|
+
// One book, two filters: the market side is every order
|
|
137
|
+
// past par, the wallet's own included, since the bot fills by price, not by owner.
|
|
138
|
+
orderPool: orders.filter((group) => isPastPar(group, exchangeRatio)),
|
|
139
|
+
poolDeposits,
|
|
140
|
+
lockUp,
|
|
141
|
+
},
|
|
142
|
+
userOrders: orders.filter((group) => group.isOwner(...locks)),
|
|
143
|
+
account: {
|
|
144
|
+
capacityCells: accounts.flatMap((account) => account.capacityCells),
|
|
145
|
+
nativeUdtCells: accounts.flatMap((account) => account.nativeUdtCells),
|
|
146
|
+
receipts: accounts.flatMap((account) => account.receipts),
|
|
147
|
+
withdrawalGroups: accounts.flatMap((account) => account.withdrawalGroups),
|
|
148
|
+
},
|
|
106
149
|
};
|
|
107
|
-
|
|
108
|
-
isCkb2Udt,
|
|
109
|
-
system.exchangeRatio,
|
|
110
|
-
amounts,
|
|
111
|
-
options,
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// If the fee meets a threshold, calculate the order maturity; otherwise, maturity is undefined.
|
|
115
|
-
const maturity =
|
|
116
|
-
ckbFee >= 10n * system.feeRate
|
|
117
|
-
? IckbSdk.maturity({ info, amounts }, system)
|
|
118
|
-
: undefined;
|
|
150
|
+
}
|
|
119
151
|
|
|
120
|
-
|
|
152
|
+
/** Every Stack cell one lock owns, classified from a single exact-lock scan. */
|
|
153
|
+
private async readLockCells(
|
|
154
|
+
client: ccc.Client,
|
|
155
|
+
lock: ccc.Script,
|
|
156
|
+
tip: ccc.ClientBlockHeader,
|
|
157
|
+
): Promise<AccountState> {
|
|
158
|
+
const scanned = await findCells(client, {
|
|
159
|
+
script: lock,
|
|
160
|
+
scriptType: "lock",
|
|
161
|
+
scriptSearchMode: "exact",
|
|
162
|
+
withData: true,
|
|
163
|
+
});
|
|
164
|
+
const cells = scanned.map(({ cell }) => cell);
|
|
165
|
+
const [receipts, withdrawalGroups] = await Promise.all([
|
|
166
|
+
this.ickbLogic.receiptsFrom(client, scanned),
|
|
167
|
+
this.ownedOwner.withdrawalGroupsFrom(client, scanned, tip),
|
|
168
|
+
]);
|
|
169
|
+
return {
|
|
170
|
+
capacityCells: cells.filter(isPlainCapacityCell),
|
|
171
|
+
nativeUdtCells: cells.filter((cell) => this.ickbUdt.isUdt(cell)),
|
|
172
|
+
receipts,
|
|
173
|
+
withdrawalGroups,
|
|
174
|
+
};
|
|
121
175
|
}
|
|
122
176
|
|
|
177
|
+
// ---- Conversion planning -----------------------------------------------------------
|
|
178
|
+
|
|
123
179
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* Depending on the order type and amount remaining, the method calculates an estimated timestamp
|
|
127
|
-
* when the order (or part thereof) might be fulfilled.
|
|
180
|
+
* Builds and completes one conversion transaction from a conversion context.
|
|
128
181
|
*
|
|
129
|
-
* @
|
|
130
|
-
*
|
|
131
|
-
*
|
|
182
|
+
* @remarks
|
|
183
|
+
* Every branch returns a transaction funded from the signer's committed cells: the
|
|
184
|
+
* candidate plans (deposit counts, or prefixes of the greedy withdrawal selection with
|
|
185
|
+
* their rebuilt remainder order) are completed most direct first and the first fundable one
|
|
186
|
+
* wins, so a wallet short of CKB degrades to fewer direct actions plus a larger standing
|
|
187
|
+
* order rather than failing. Failure results are
|
|
188
|
+
* expected planning outcomes; when no plan can be funded the last completion error throws.
|
|
132
189
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"
|
|
145
|
-
? o.amounts
|
|
146
|
-
: { ckbValue: o.ckbUnoccupied, udtValue: o.udtValue };
|
|
147
|
-
|
|
148
|
-
// Dual-ratio orders have no fixed maturity.
|
|
149
|
-
if (info.isDualRatio()) {
|
|
150
|
-
return;
|
|
190
|
+
public async buildConversionTransaction(
|
|
191
|
+
options: ConversionTransactionOptions,
|
|
192
|
+
): Promise<ConversionTransactionResult> {
|
|
193
|
+
const { amount, context, direction } = options;
|
|
194
|
+
if (amount < 0n) {
|
|
195
|
+
return conversionFailure("amount-negative", context.estimatedMaturity);
|
|
196
|
+
}
|
|
197
|
+
if (direction === "ckb-to-ickb" && amount > context.ckbAvailable) {
|
|
198
|
+
return conversionFailure("insufficient-ckb", context.estimatedMaturity);
|
|
199
|
+
}
|
|
200
|
+
if (direction === "ickb-to-ckb" && amount > context.ickbAvailable) {
|
|
201
|
+
return conversionFailure("insufficient-ickb", context.estimatedMaturity);
|
|
151
202
|
}
|
|
152
203
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// If order is already fulfilled.
|
|
204
|
+
const resolved = {
|
|
205
|
+
...options,
|
|
206
|
+
lock: options.lock ?? (await options.signer.getRecommendedAddressObj()).script,
|
|
207
|
+
};
|
|
158
208
|
if (amount === 0n) {
|
|
159
|
-
return
|
|
209
|
+
return this.buildCollectOnlyConversion(resolved);
|
|
160
210
|
}
|
|
211
|
+
return direction === "ckb-to-ickb"
|
|
212
|
+
? this.buildCkbToIckbConversion(resolved)
|
|
213
|
+
: this.buildIckbToCkbConversion(resolved);
|
|
214
|
+
}
|
|
161
215
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
} else {
|
|
176
|
-
// Conversely for UDT to CKB orders.
|
|
177
|
-
if (isCkb2Udt || a.udt2CkbCompare(b) < 0) {
|
|
178
|
-
udt += o.udtValue;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// Adjust ckb by the converted UDT amount.
|
|
183
|
-
ckb -= convert(false, udt, exchangeRatio);
|
|
184
|
-
|
|
185
|
-
// Minimum maturity of 10 minutes (in milliseconds).
|
|
186
|
-
let maturity = 10n * 60n * 1000n;
|
|
187
|
-
if (isCkb2Udt) {
|
|
188
|
-
// For CKB to UDT orders, extend maturity based on the CKB amount.
|
|
189
|
-
if (ckb > 0n) {
|
|
190
|
-
maturity *= 1n + ckb / ccc.fixedPointFrom("200000");
|
|
191
|
-
}
|
|
192
|
-
return maturity + ("info" in o ? BigInt(Date.now()) : tip.timestamp);
|
|
216
|
+
private async buildCollectOnlyConversion(
|
|
217
|
+
options: ResolvedConversionOptions,
|
|
218
|
+
): Promise<ConversionTransactionResult> {
|
|
219
|
+
const { context, lock, signer } = options;
|
|
220
|
+
const tx = this.buildBaseTransaction(ccc.Transaction.default(), context);
|
|
221
|
+
// An empty collection to a lock of the signer's own would only compact the account;
|
|
222
|
+
// to another lock it moves the liquid cells, which completion builds from the sweep.
|
|
223
|
+
if (
|
|
224
|
+
!hasTransactionActivity(tx) &&
|
|
225
|
+
(context.cells.length === 0 || (await isOwnLock(signer, lock)))
|
|
226
|
+
) {
|
|
227
|
+
return conversionFailure("nothing-to-do", context.estimatedMaturity);
|
|
193
228
|
}
|
|
229
|
+
const completed = await this.completeConversion(tx, options);
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
tx: completed,
|
|
233
|
+
estimatedMaturity: context.estimatedMaturity,
|
|
234
|
+
conversionKind: "collect-only",
|
|
235
|
+
isSweepComplete: sweepsAll(completed, context.cells),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
194
238
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
239
|
+
private async buildCkbToIckbConversion(
|
|
240
|
+
options: ResolvedConversionOptions,
|
|
241
|
+
): Promise<ConversionTransactionResult> {
|
|
242
|
+
const { context, lock } = options;
|
|
243
|
+
// A plan is dropped only for an unrepresentable remainder order.
|
|
244
|
+
const plans = ckbToIckbConversionPlans(options);
|
|
245
|
+
if (plans.length === 0) {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
reason: "amount-too-small",
|
|
249
|
+
estimatedMaturity: context.estimatedMaturity,
|
|
250
|
+
minimum: minimumOrderAmount(true, context.system),
|
|
251
|
+
};
|
|
199
252
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
253
|
+
const { candidate: plan, tx } = await completeFirstFundable(
|
|
254
|
+
plans,
|
|
255
|
+
(candidate) => {
|
|
256
|
+
let partial = this.buildBaseTransaction(ccc.Transaction.default(), context);
|
|
257
|
+
if (candidate.depositCount > 0) {
|
|
258
|
+
partial = this.ickbLogic.deposit(
|
|
259
|
+
partial,
|
|
260
|
+
candidate.depositCount,
|
|
261
|
+
candidate.depositCapacity,
|
|
262
|
+
lock,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (candidate.order !== undefined) {
|
|
266
|
+
partial = this.order.mint(
|
|
267
|
+
partial,
|
|
268
|
+
lock,
|
|
269
|
+
candidate.order.estimate.info,
|
|
270
|
+
candidate.order.amounts,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return partial;
|
|
274
|
+
},
|
|
275
|
+
async (partial) => this.completeConversion(partial, options),
|
|
207
276
|
);
|
|
208
|
-
|
|
209
|
-
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
tx,
|
|
280
|
+
estimatedMaturity: plan.estimatedMaturity,
|
|
281
|
+
conversionKind: conversionKind(plan.depositCount > 0, plan.order !== undefined),
|
|
282
|
+
isSweepComplete: sweepsAll(tx, context.cells),
|
|
283
|
+
};
|
|
210
284
|
}
|
|
211
285
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
286
|
+
private async buildIckbToCkbConversion(
|
|
287
|
+
options: ResolvedConversionOptions,
|
|
288
|
+
): Promise<ConversionTransactionResult> {
|
|
289
|
+
const { context, lock } = options;
|
|
290
|
+
const plans = ickbToCkbConversionPlans(options, context.system.poolDeposits);
|
|
291
|
+
if (plans.length === 0) {
|
|
292
|
+
return {
|
|
293
|
+
ok: false,
|
|
294
|
+
reason: "amount-too-small",
|
|
295
|
+
estimatedMaturity: context.estimatedMaturity,
|
|
296
|
+
minimum: minimumOrderAmount(false, context.system),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const { candidate: plan, tx } = await completeFirstFundable(
|
|
300
|
+
plans,
|
|
301
|
+
(candidate) => {
|
|
302
|
+
let partial = this.buildBaseTransaction(ccc.Transaction.default(), context, {
|
|
303
|
+
deposits: candidate.selectedDeposits,
|
|
304
|
+
lock,
|
|
305
|
+
});
|
|
306
|
+
if (candidate.order !== undefined) {
|
|
307
|
+
partial = this.order.mint(
|
|
308
|
+
partial,
|
|
309
|
+
lock,
|
|
310
|
+
candidate.order.estimate.info,
|
|
311
|
+
candidate.order.amounts,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return partial;
|
|
315
|
+
},
|
|
316
|
+
async (partial) => this.completeConversion(partial, options),
|
|
317
|
+
);
|
|
318
|
+
const notice = plan.order?.estimate.notice;
|
|
319
|
+
const broadcastBefore = broadcastDeadline(
|
|
320
|
+
plan.selectedDeposits.map((deposit) => deposit.claimEpoch),
|
|
321
|
+
context.system.lockUp,
|
|
322
|
+
);
|
|
323
|
+
return {
|
|
324
|
+
ok: true,
|
|
325
|
+
tx,
|
|
326
|
+
estimatedMaturity: plan.estimatedMaturity,
|
|
327
|
+
conversionKind: conversionKind(
|
|
328
|
+
plan.selectedDeposits.length > 0,
|
|
329
|
+
plan.order !== undefined,
|
|
330
|
+
),
|
|
331
|
+
...(notice === undefined ? {} : { conversionNotice: notice }),
|
|
332
|
+
isSweepComplete: sweepsAll(tx, context.cells),
|
|
333
|
+
...(broadcastBefore === undefined ? {} : { broadcastBefore }),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
240
336
|
|
|
241
|
-
|
|
337
|
+
private async completeConversion(
|
|
338
|
+
tx: ccc.Transaction,
|
|
339
|
+
options: ResolvedConversionOptions,
|
|
340
|
+
): Promise<ccc.Transaction> {
|
|
341
|
+
return this.completeTransaction(tx, {
|
|
342
|
+
signer: options.signer,
|
|
343
|
+
lock: options.lock,
|
|
344
|
+
feeRate: options.context.system.feeRate,
|
|
345
|
+
cells: options.context.cells,
|
|
346
|
+
protocolCells: [
|
|
347
|
+
...options.context.receipts,
|
|
348
|
+
...options.context.system.poolDeposits,
|
|
349
|
+
],
|
|
350
|
+
tip: options.context.system.tip,
|
|
351
|
+
});
|
|
242
352
|
}
|
|
243
353
|
|
|
354
|
+
// ---- Transaction building ----------------------------------------------------------
|
|
355
|
+
|
|
244
356
|
/**
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* For each order group, if the option is set to process fulfilled orders only,
|
|
248
|
-
* it filters accordingly. Then, for every valid group, the master and order cells are added
|
|
249
|
-
* as inputs to the transaction.
|
|
250
|
-
*
|
|
251
|
-
* @param tx - The smart transaction to which the inputs are added.
|
|
252
|
-
* @param groups - An array of order groups to be melted.
|
|
253
|
-
* @param options - Optional parameters:
|
|
254
|
-
* - isFulfilledOnly: If true, only order groups with fully or partially fulfilled orders are processed.
|
|
357
|
+
* Adds the context's collect steps to a partial transaction: the withdrawal requests when
|
|
358
|
+
* given, then the collectable orders, the receipts, and the ready withdrawals.
|
|
255
359
|
*
|
|
256
|
-
* @
|
|
360
|
+
* @remarks
|
|
361
|
+
* The result is still partial. Callers should use `completeTransaction` before
|
|
362
|
+
* signing and sending. The withdrawals move their deposit headers to the front of
|
|
363
|
+
* `headerDeps`, so the given transaction must not reference a header by position.
|
|
257
364
|
*/
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
365
|
+
public buildBaseTransaction(
|
|
366
|
+
txLike: ccc.TransactionLike,
|
|
367
|
+
steps: CollectSteps,
|
|
368
|
+
withdrawalRequest?: { deposits: IckbDepositCell[]; lock: ccc.Script },
|
|
369
|
+
): ccc.Transaction {
|
|
370
|
+
let tx = ccc.Transaction.from(txLike);
|
|
371
|
+
if (withdrawalRequest !== undefined && withdrawalRequest.deposits.length > 0) {
|
|
372
|
+
tx = this.ownedOwner.requestWithdrawal(
|
|
373
|
+
tx,
|
|
374
|
+
withdrawalRequest.deposits,
|
|
375
|
+
withdrawalRequest.lock,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (steps.availableOrders.length > 0) {
|
|
379
|
+
tx = this.order.melt(tx, steps.availableOrders);
|
|
380
|
+
}
|
|
381
|
+
if (steps.receipts.length > 0) {
|
|
382
|
+
tx = this.ickbLogic.completeDeposit(tx, steps.receipts);
|
|
383
|
+
}
|
|
384
|
+
if (steps.readyWithdrawals.length > 0) {
|
|
385
|
+
tx = this.ownedOwner.withdraw(tx, steps.readyWithdrawals);
|
|
386
|
+
}
|
|
387
|
+
return tx;
|
|
266
388
|
}
|
|
267
389
|
|
|
268
390
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* The method performs the following:
|
|
272
|
-
* - Obtains the current block tip and calculates the exchange ratio.
|
|
273
|
-
* - Fetches available CKB and the maturing CKB based on bot capacities and deposit snapshots.
|
|
274
|
-
* - Filters orders into user-owned and system orders based on the provided locks.
|
|
275
|
-
* - Estimates user-owned orders maturity.
|
|
391
|
+
* Completes iCKB inputs, iCKB change, plain change, and the fee from the cells it is given.
|
|
276
392
|
*
|
|
277
|
-
* @
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
393
|
+
* @remarks Completion never scans. `cells` are the signer's known liquid cells, plain
|
|
394
|
+
* CKB and iCKB, from the account state already read: the largest ones fund what the
|
|
395
|
+
* outputs need, the rest ride along as a sweep while the transaction has fewer than
|
|
396
|
+
* {@link SWEEP_INPUT_LIMIT} inputs, so every own transaction compacts the account.
|
|
397
|
+
* Ordinary change is always a plain cell; existing outputs
|
|
398
|
+
* are never reinterpreted or resized as fee change. This does not sign or send.
|
|
282
399
|
*/
|
|
283
|
-
async
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
): Promise<
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const systemOrders: OrderCell[] = [];
|
|
300
|
-
for (const group of orders) {
|
|
301
|
-
if (group.isOwner(...locks)) {
|
|
302
|
-
userOrders.push(group);
|
|
400
|
+
public async completeTransaction(
|
|
401
|
+
txLike: ccc.TransactionLike,
|
|
402
|
+
options: CompleteIckbTransactionOptions,
|
|
403
|
+
): Promise<ccc.Transaction> {
|
|
404
|
+
const { signer, feeRate } = options;
|
|
405
|
+
const tx = ccc.Transaction.from(txLike).clone();
|
|
406
|
+
tx.addCellDeps(this.ickbUdt.cellDeps);
|
|
407
|
+
const changeLock = options.lock ?? (await signer.getRecommendedAddressObj()).script;
|
|
408
|
+
// Inputs come from one state read, each cell once, and each action spends its own
|
|
409
|
+
// cell kind, so a repeated out point is a builder bug: this is the one place that
|
|
410
|
+
// names it, the node would refuse the transaction anyway.
|
|
411
|
+
const spent = new Set<string>();
|
|
412
|
+
for (const { previousOutput } of tx.inputs) {
|
|
413
|
+
const key = previousOutput.toHex();
|
|
414
|
+
if (spent.has(key)) {
|
|
415
|
+
throw new Error(`Input ${key} is spent twice`);
|
|
303
416
|
}
|
|
417
|
+
spent.add(key);
|
|
418
|
+
}
|
|
419
|
+
const unspent = options.cells.filter((cell) => !spent.has(cell.outPoint.toHex()));
|
|
420
|
+
const ickbCells = unspent
|
|
421
|
+
.filter((cell) => this.ickbUdt.isUdt(cell))
|
|
422
|
+
.toSorted((left, right) => compareBigInt(udtBalance(right), udtBalance(left)));
|
|
423
|
+
const plainCells = unspent
|
|
424
|
+
.filter(isPlainCapacityCell)
|
|
425
|
+
.toSorted((left, right) =>
|
|
426
|
+
compareBigInt(right.cellOutput.capacity, left.cellOutput.capacity),
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
await this.completeIckb(
|
|
430
|
+
tx,
|
|
431
|
+
signer.client,
|
|
432
|
+
changeLock,
|
|
433
|
+
ickbCells,
|
|
434
|
+
options.protocolCells,
|
|
435
|
+
);
|
|
436
|
+
// Plain CKB: the sweep first, then whatever the fee still needs beyond the limit.
|
|
437
|
+
const swept = sweep(tx, plainCells);
|
|
438
|
+
// Last of the header deps, so no index another step names shifts, and before the fee so
|
|
439
|
+
// its 32 bytes are paid for.
|
|
440
|
+
pushHeaderDep(tx, options.tip.hash);
|
|
441
|
+
await completeFee(tx, signer, changeLock, feeRate, plainCells.slice(swept));
|
|
442
|
+
assertDaoOutputLimit(tx, this.ickbLogic.dao.script);
|
|
443
|
+
return tx;
|
|
444
|
+
}
|
|
304
445
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
446
|
+
/** iCKB: what the outputs need first, then the sweep while the limit allows, then change. */
|
|
447
|
+
private async completeIckb(
|
|
448
|
+
tx: ccc.Transaction,
|
|
449
|
+
client: ccc.Client,
|
|
450
|
+
changeLock: ccc.Script,
|
|
451
|
+
ickbCells: readonly ccc.Cell[],
|
|
452
|
+
protocolCells: readonly ProtocolCell[],
|
|
453
|
+
): Promise<void> {
|
|
454
|
+
const required = tx.getOutputsUdtBalance(this.ickbUdt.script);
|
|
455
|
+
let balance = await this.ickbUdt.inputBalance(tx, client, protocolCells);
|
|
456
|
+
for (const cell of ickbCells) {
|
|
457
|
+
if (balance >= required && tx.inputs.length >= SWEEP_INPUT_LIMIT) {
|
|
458
|
+
break;
|
|
312
459
|
}
|
|
460
|
+
tx.addInput(cell);
|
|
461
|
+
balance += udtBalance(cell);
|
|
313
462
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
exchangeRatio,
|
|
319
|
-
orderPool: systemOrders,
|
|
320
|
-
ckbAvailable,
|
|
321
|
-
ckbMaturing,
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
// Estimates user orders maturity.
|
|
325
|
-
for (const { order } of userOrders) {
|
|
326
|
-
order.maturity = IckbSdk.maturity(order, system);
|
|
463
|
+
if (balance < required) {
|
|
464
|
+
throw new IckbError(`Insufficient iCKB, need ${String(required - balance)} more`, {
|
|
465
|
+
code: "insufficient_ickb",
|
|
466
|
+
});
|
|
327
467
|
}
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
system,
|
|
331
|
-
user: {
|
|
332
|
-
orders: userOrders,
|
|
333
|
-
},
|
|
334
|
-
};
|
|
468
|
+
this.ickbUdt.addChange(tx, changeLock, balance - required);
|
|
335
469
|
}
|
|
470
|
+
}
|
|
336
471
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
* - Fetches bot withdrawal requests and deposit snapshots.
|
|
342
|
-
* - Aggregates available CKB balances from bot capacities.
|
|
343
|
-
* - Calculates maturing CKB values (with their expected maturity timestamps)
|
|
344
|
-
* based on deposit pool snapshots or via direct deposit cell lookups.
|
|
345
|
-
* - Sorts and cumulatively sums the maturing values for later lookup.
|
|
346
|
-
*
|
|
347
|
-
* @param client - The blockchain client used for fetching data.
|
|
348
|
-
* @param tip - The current block tip header.
|
|
349
|
-
* @returns A Promise that resolves with:
|
|
350
|
-
* - ckbAvailable: The total available CKB (as a FixedPoint).
|
|
351
|
-
* - ckbMaturing: An array of maturing CKB objects containing the cumulative CKB
|
|
352
|
-
* and the associated maturity timestamp.
|
|
353
|
-
*/
|
|
354
|
-
private async getCkb(
|
|
355
|
-
client: ccc.Client,
|
|
356
|
-
tip: ccc.ClientBlockHeader,
|
|
357
|
-
): Promise<{
|
|
358
|
-
ckbAvailable: ccc.FixedPoint;
|
|
359
|
-
ckbMaturing: CkbCumulative[];
|
|
360
|
-
}> {
|
|
361
|
-
const opts = {
|
|
362
|
-
onChain: true,
|
|
363
|
-
tip,
|
|
364
|
-
};
|
|
472
|
+
/** Whether the transaction already carries any input or output. */
|
|
473
|
+
function hasTransactionActivity(tx: ccc.Transaction): boolean {
|
|
474
|
+
return tx.inputs.length > 0 || tx.outputs.length > 0;
|
|
475
|
+
}
|
|
365
476
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
477
|
+
/** An order the market can fill at a gain: priced past the DAO ratio on its matchable side. */
|
|
478
|
+
function isPastPar(group: OrderGroup, exchangeRatio: Ratio): boolean {
|
|
479
|
+
const { order } = group;
|
|
480
|
+
const { info } = order.data;
|
|
481
|
+
return (
|
|
482
|
+
(order.isCkb2UdtMatchable() && info.ckbToUdt.compare(exchangeRatio) < 0) ||
|
|
483
|
+
(order.isUdt2CkbMatchable() && exchangeRatio.compare(info.udtToCkb) < 0)
|
|
484
|
+
);
|
|
485
|
+
}
|
|
370
486
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
for await (const c of this.capacity.findCapacities(
|
|
378
|
-
client,
|
|
379
|
-
this.bots,
|
|
380
|
-
opts,
|
|
381
|
-
)) {
|
|
382
|
-
const key = hexFrom(c.cell.cellOutput.lock);
|
|
383
|
-
const ckb = (bot2Ckb.get(key) ?? reserved) + c.ckbValue;
|
|
384
|
-
bot2Ckb.set(key, ckb);
|
|
385
|
-
|
|
386
|
-
// Find the most recent deposit pool snapshot from bot cell output data.
|
|
387
|
-
const outputData = c.cell.outputData;
|
|
388
|
-
if (outputData.length % 256 === 2) {
|
|
389
|
-
const h = await getHeader(client, {
|
|
390
|
-
type: "txHash",
|
|
391
|
-
value: c.cell.outPoint.txHash,
|
|
392
|
-
});
|
|
393
|
-
const e = Epoch.from(h.epoch);
|
|
394
|
-
if (poolSnapshotEpoch.compare(e) < 0) {
|
|
395
|
-
poolSnapshotHex = outputData;
|
|
396
|
-
poolSnapshotEpoch = e;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
487
|
+
function conversionFailure(
|
|
488
|
+
reason: Exclude<ConversionTransactionFailureReason, "amount-too-small">,
|
|
489
|
+
estimatedMaturity: bigint,
|
|
490
|
+
): ConversionTransactionResult {
|
|
491
|
+
return { ok: false, reason, estimatedMaturity };
|
|
492
|
+
}
|
|
400
493
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
494
|
+
/** A plan always carries a deposit or an order, else it would be a collection. */
|
|
495
|
+
function conversionKind(
|
|
496
|
+
hasDirect: boolean,
|
|
497
|
+
hasOrder: boolean,
|
|
498
|
+
): "direct" | "order" | "direct-plus-order" {
|
|
499
|
+
if (hasDirect && hasOrder) {
|
|
500
|
+
return "direct-plus-order";
|
|
501
|
+
}
|
|
502
|
+
return hasDirect ? "direct" : "order";
|
|
503
|
+
}
|
|
413
504
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
505
|
+
/** Whether every liquid cell became an input; false when the input limit cut the sweep. */
|
|
506
|
+
function sweepsAll(tx: ccc.Transaction, cells: readonly ccc.Cell[]): boolean {
|
|
507
|
+
const spent = new Set(tx.inputs.map((input) => input.previousOutput.toHex()));
|
|
508
|
+
return cells.every((cell) => spent.has(cell.outPoint.toHex()));
|
|
509
|
+
}
|
|
420
510
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (ckb > 0n) {
|
|
425
|
-
ckbAvailable += ckb;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
511
|
+
async function isOwnLock(signer: ccc.Signer, lock: ccc.Script): Promise<boolean> {
|
|
512
|
+
return (await signer.getAddressObjs()).some(({ script }) => script.eq(lock));
|
|
513
|
+
}
|
|
428
514
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (poolSnapshotHex !== "0x") {
|
|
433
|
-
const eNumber = tip.epoch[0];
|
|
434
|
-
let start = Epoch.from([eNumber - (eNumber % 180n), 0n, 1n]);
|
|
435
|
-
const step = Epoch.from([0n, 180n, 1024n]);
|
|
436
|
-
const depositSize = convert(false, ICKB_DEPOSIT_CAP, tip);
|
|
437
|
-
for (const binAmount of PoolSnapshot.decode(poolSnapshotHex)) {
|
|
438
|
-
const end = start.add(step);
|
|
439
|
-
|
|
440
|
-
if (binAmount > 0) {
|
|
441
|
-
ckbMaturing.push({
|
|
442
|
-
ckbValue: BigInt(binAmount) * depositSize,
|
|
443
|
-
maturity:
|
|
444
|
-
tipEpoch.compare(tipEpoch) < 0
|
|
445
|
-
? // If the bin has already started, assume worst-case timing.
|
|
446
|
-
end.add(oneCycle).toUnix(tip)
|
|
447
|
-
: // Otherwise, use the bin end as the maturity.
|
|
448
|
-
end.toUnix(tip),
|
|
449
|
-
});
|
|
450
|
-
}
|
|
515
|
+
function udtBalance(cell: ccc.Cell): ccc.Num {
|
|
516
|
+
return ccc.udtBalanceFrom(cell.outputData);
|
|
517
|
+
}
|
|
451
518
|
|
|
452
|
-
|
|
519
|
+
/** Adds cells while the input count stays under the limit; returns how many were added. */
|
|
520
|
+
function sweep(tx: ccc.Transaction, cells: readonly ccc.Cell[]): number {
|
|
521
|
+
const added = Math.min(cells.length, Math.max(0, SWEEP_INPUT_LIMIT - tx.inputs.length));
|
|
522
|
+
for (const cell of cells.slice(0, added)) {
|
|
523
|
+
tx.addInput(cell);
|
|
524
|
+
}
|
|
525
|
+
return added;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Completes the fee, adding reserve cells one at a time while capacity is short. */
|
|
529
|
+
async function completeFee(
|
|
530
|
+
tx: ccc.Transaction,
|
|
531
|
+
signer: ccc.Signer,
|
|
532
|
+
changeLock: ccc.Script,
|
|
533
|
+
feeRate: ccc.Num,
|
|
534
|
+
reserve: readonly ccc.Cell[],
|
|
535
|
+
): Promise<void> {
|
|
536
|
+
const remaining = [...reserve];
|
|
537
|
+
for (;;) {
|
|
538
|
+
try {
|
|
539
|
+
await tx.completeFeeChangeToLock(signer, changeLock, feeRate, undefined, {
|
|
540
|
+
shouldAddInputs: false,
|
|
541
|
+
});
|
|
542
|
+
return;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (!(error instanceof ccc.ErrorTransactionInsufficientCapacity)) {
|
|
545
|
+
throw error;
|
|
453
546
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
maturity: d.maturity.toUnix(tip),
|
|
547
|
+
const cell = remaining.shift();
|
|
548
|
+
if (cell === undefined) {
|
|
549
|
+
throw new IckbError(error.message, {
|
|
550
|
+
code: "insufficient_capacity",
|
|
551
|
+
cause: error,
|
|
460
552
|
});
|
|
461
553
|
}
|
|
554
|
+
tx.addInput(cell);
|
|
462
555
|
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
463
558
|
|
|
464
|
-
|
|
465
|
-
|
|
559
|
+
/** Stable machine-readable SDK failures. */
|
|
560
|
+
export type IckbErrorCode = "insufficient_capacity" | "insufficient_ickb";
|
|
466
561
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
cumulative += ckbValue;
|
|
472
|
-
ckbCumulativeMaturing.push({ ckbCumulative: cumulative, maturity });
|
|
473
|
-
}
|
|
562
|
+
/** Typed SDK failure with a stable machine-readable code. */
|
|
563
|
+
export class IckbError extends Error {
|
|
564
|
+
/** Stable failure code for callers and observability. */
|
|
565
|
+
public readonly code: IckbErrorCode;
|
|
474
566
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
567
|
+
/** Creates a typed SDK failure while preserving its cause. */
|
|
568
|
+
constructor(message: string, options: ErrorOptions & { code: IckbErrorCode }) {
|
|
569
|
+
super(message, options);
|
|
570
|
+
this.name = "IckbError";
|
|
571
|
+
this.code = options.code;
|
|
479
572
|
}
|
|
480
573
|
}
|
|
481
574
|
|
|
482
575
|
/**
|
|
483
|
-
*
|
|
576
|
+
* Completes the first candidate that the real completer can fund.
|
|
577
|
+
*
|
|
578
|
+
* @remarks
|
|
579
|
+
* Only completion knows what a transaction costs once markers, remainder orders, change, and
|
|
580
|
+
* fee are in, so no count is computed up front: each candidate is built and completed in turn.
|
|
581
|
+
* Capacity, DAO output-limit and DAO header-index failures advance
|
|
582
|
+
* the walk; transport, scan, signer, and malformed-transaction errors propagate. Exhausting
|
|
583
|
+
* the candidates throws the last advancing failure.
|
|
484
584
|
*/
|
|
485
|
-
export
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
ckbMaturing: CkbCumulative[];
|
|
585
|
+
export async function completeFirstFundable<T>(
|
|
586
|
+
candidates: Iterable<T>,
|
|
587
|
+
build: (candidate: T) => ccc.Transaction,
|
|
588
|
+
complete: (tx: ccc.Transaction) => Promise<ccc.Transaction>,
|
|
589
|
+
): Promise<{ candidate: T; tx: ccc.Transaction }> {
|
|
590
|
+
let error: Error | undefined;
|
|
591
|
+
for (const candidate of candidates) {
|
|
592
|
+
try {
|
|
593
|
+
return { candidate, tx: await complete(build(candidate)) };
|
|
594
|
+
} catch (failure) {
|
|
595
|
+
if (!isFundabilityFailure(failure)) {
|
|
596
|
+
throw failure;
|
|
597
|
+
}
|
|
598
|
+
error = failure;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
throw error ?? new Error("No candidate could be completed");
|
|
503
602
|
}
|
|
504
603
|
|
|
505
604
|
/**
|
|
506
|
-
*
|
|
605
|
+
* A failure the walk advances past: the candidate costs more than the account funds, or
|
|
606
|
+
* needs more DAO outputs or deposit-header slots than one transaction has.
|
|
507
607
|
*/
|
|
508
|
-
export
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
608
|
+
export function isFundabilityFailure(error: unknown): error is Error {
|
|
609
|
+
return (
|
|
610
|
+
error instanceof IckbError ||
|
|
611
|
+
error instanceof ccc.ErrorTransactionInsufficientCapacity ||
|
|
612
|
+
error instanceof DaoOutputLimitError ||
|
|
613
|
+
error instanceof DaoHeaderIndexError
|
|
614
|
+
);
|
|
513
615
|
}
|