@ickb/sdk 1000.0.81 → 9000.0.1
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 +81 -32
- package/dist/constants.d.ts +13 -22
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +80 -76
- package/dist/constants.js.map +1 -1
- package/dist/conversion/estimate.d.ts +39 -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 +112 -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 +22 -0
- package/dist/conversion/projection.d.ts.map +1 -0
- package/dist/conversion/projection.js +91 -0
- package/dist/conversion/projection.js.map +1 -0
- package/dist/conversion/types.d.ts +252 -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 +36 -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 +86 -0
- package/dist/dao.d.ts.map +1 -0
- package/dist/dao.js +113 -0
- package/dist/dao.js.map +1 -0
- package/dist/index.d.ts +51 -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 +85 -0
- package/dist/logic.d.ts.map +1 -0
- package/dist/logic.js +184 -0
- package/dist/logic.js.map +1 -0
- package/dist/order/cells.d.ts +207 -0
- package/dist/order/cells.d.ts.map +1 -0
- package/dist/order/cells.js +427 -0
- package/dist/order/cells.js.map +1 -0
- package/dist/order/conversion.d.ts +43 -0
- package/dist/order/conversion.d.ts.map +1 -0
- package/dist/order/conversion.js +123 -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 +99 -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 +123 -0
- package/dist/order/matcher.js.map +1 -0
- package/dist/order/order.d.ts +59 -0
- package/dist/order/order.d.ts.map +1 -0
- package/dist/order/order.js +239 -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 +64 -0
- package/dist/order/order_data.js.map +1 -0
- package/dist/order/ratio.d.ts +42 -0
- package/dist/order/ratio.d.ts.map +1 -0
- package/dist/order/ratio.js +82 -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 +54 -0
- package/dist/order/relative.js.map +1 -0
- package/dist/owned_owner.d.ts +113 -0
- package/dist/owned_owner.d.ts.map +1 -0
- package/dist/owned_owner.js +280 -0
- package/dist/owned_owner.js.map +1 -0
- package/dist/sdk.d.ts +71 -42
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +388 -209
- package/dist/sdk.js.map +1 -1
- package/dist/send/sign_and_send_transaction.d.ts +46 -0
- package/dist/send/sign_and_send_transaction.d.ts.map +1 -0
- package/dist/send/sign_and_send_transaction.js +101 -0
- package/dist/send/sign_and_send_transaction.js.map +1 -0
- package/dist/send/wait_transaction.d.ts +41 -0
- package/dist/send/wait_transaction.d.ts.map +1 -0
- package/dist/send/wait_transaction.js +204 -0
- package/dist/send/wait_transaction.js.map +1 -0
- package/dist/udt.d.ts +123 -0
- package/dist/udt.d.ts.map +1 -0
- package/dist/udt.js +244 -0
- package/dist/udt.js.map +1 -0
- package/dist/utils/chain.d.ts +38 -0
- package/dist/utils/chain.d.ts.map +1 -0
- package/dist/utils/chain.js +20 -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 +66 -0
- package/dist/utils/codec.js.map +1 -0
- package/dist/utils/transaction_header.d.ts +17 -0
- package/dist/utils/transaction_header.d.ts.map +1 -0
- package/dist/utils/transaction_header.js +44 -0
- package/dist/utils/transaction_header.js.map +1 -0
- package/dist/utils/utils.d.ts +126 -0
- package/dist/utils/utils.d.ts.map +1 -0
- package/dist/utils/utils.js +151 -0
- package/dist/utils/utils.js.map +1 -0
- package/package.json +19 -26
- package/src/constants.ts +84 -181
- package/src/conversion/estimate.ts +157 -0
- package/src/conversion/maturity.ts +159 -0
- package/src/conversion/plans.ts +144 -0
- package/src/conversion/projection.ts +128 -0
- package/src/conversion/types.ts +287 -0
- package/src/conversion/withdrawal_ring.ts +126 -0
- package/src/dao.ts +172 -0
- package/src/index.ts +89 -3
- package/src/logic.ts +295 -0
- package/src/order/cells.ts +520 -0
- package/src/order/conversion.ts +173 -0
- package/src/order/fee.ts +30 -0
- package/src/order/fill.ts +83 -0
- package/src/order/info.ts +178 -0
- package/src/order/master.ts +42 -0
- package/src/order/matcher.ts +191 -0
- package/src/order/order.ts +302 -0
- package/src/order/order_data.ts +128 -0
- package/src/order/ratio.ts +144 -0
- package/src/order/relative.ts +106 -0
- package/src/owned_owner.ts +394 -0
- package/src/sdk.ts +555 -440
- package/src/send/sign_and_send_transaction.ts +120 -0
- package/src/send/wait_transaction.ts +271 -0
- package/src/udt.ts +317 -0
- package/src/utils/chain.ts +43 -0
- package/src/utils/codec.ts +74 -0
- package/src/utils/transaction_header.ts +66 -0
- package/src/utils/utils.ts +237 -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/dist/sdk.js
CHANGED
|
@@ -1,242 +1,421 @@
|
|
|
1
1
|
import { ccc } from "@ckb-ccc/core";
|
|
2
|
-
import { collect, CapacityManager, SmartTransaction, binarySearch, hexFrom, getHeader, Epoch, } from "@ickb/utils";
|
|
3
|
-
import { convert, ICKB_DEPOSIT_CAP, ickbExchangeRatio, } from "@ickb/core";
|
|
4
|
-
import { Info, OrderManager, Ratio, } from "@ickb/order";
|
|
5
2
|
import { getConfig } from "./constants.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
enumerable: true,
|
|
35
|
-
configurable: true,
|
|
36
|
-
writable: true,
|
|
37
|
-
value: bots
|
|
38
|
-
});
|
|
3
|
+
import { minimumOrderAmount } from "./conversion/estimate.js";
|
|
4
|
+
import { ckbToIckbConversionPlans, ickbToCkbConversionPlans, } from "./conversion/plans.js";
|
|
5
|
+
import { assertDaoOutputLimit, broadcastDeadline, DaoHeaderIndexError, DaoOutputLimitError, } from "./dao.js";
|
|
6
|
+
import { Ratio } from "./order/ratio.js";
|
|
7
|
+
import { ickbExchangeRatio } from "./udt.js";
|
|
8
|
+
import { compareBigInt, findCells, isPlainCapacityCell, unique } from "./utils/utils.js";
|
|
9
|
+
/**
|
|
10
|
+
* Input count up to which one own transaction sweeps liquid cells.
|
|
11
|
+
*
|
|
12
|
+
* @remarks A compaction target, not a validity bound: required funding and the fee may go
|
|
13
|
+
* past it. Counted rather than measured in bytes because a same-lock input costs a fixed
|
|
14
|
+
* 44 bytes (52 with an extra witness slot), so 1,000 inputs stay near 50 KB, far under the
|
|
15
|
+
* node's 512,000-byte limit, and near the one measured point (1,000 inputs: 44 KB, about
|
|
16
|
+
* 180 ms of local completion). A miner paying the bot adds about forty cellbase cells per
|
|
17
|
+
* turn, which this outpaces; a larger backlog takes several transactions.
|
|
18
|
+
*/
|
|
19
|
+
export const SWEEP_INPUT_LIMIT = 1000;
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow -- Preserve the runtime constructor name.
|
|
21
|
+
const IckbSdkImplementation = class IckbSdk {
|
|
22
|
+
ickbUdt;
|
|
23
|
+
ownedOwner;
|
|
24
|
+
ickbLogic;
|
|
25
|
+
order;
|
|
26
|
+
constructor({ ickbUdt, ownedOwner, ickbLogic, order }) {
|
|
27
|
+
this.ickbUdt = ickbUdt;
|
|
28
|
+
this.ownedOwner = ownedOwner;
|
|
29
|
+
this.ickbLogic = ickbLogic;
|
|
30
|
+
this.order = order;
|
|
39
31
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return new IckbSdk(
|
|
32
|
+
/** Creates the SDK for one chain's deployment. */
|
|
33
|
+
static fromChain(chain) {
|
|
34
|
+
return new IckbSdk(getConfig(chain));
|
|
43
35
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
// ---- State reads -------------------------------------------------------------------
|
|
37
|
+
/**
|
|
38
|
+
* Reads system, user-order, and account state against one sampled tip, under the
|
|
39
|
+
* caller's withdrawal timing rules (`BOT_LOCK_UP` or `WALLET_LOCK_UP`).
|
|
40
|
+
*
|
|
41
|
+
* @remarks Every read is complete and uncapped: a large book or pool costs a slower
|
|
42
|
+
* read, never a partial or failed one. Each account lock is enumerated by one uncached
|
|
43
|
+
* unfiltered exact-lock scan and classified client-side.
|
|
44
|
+
*/
|
|
45
|
+
async getL1AccountState(client, locks, lockUp) {
|
|
46
|
+
const tip = await client.getTipHeader();
|
|
47
|
+
const exchangeRatio = Ratio.from(ickbExchangeRatio(tip));
|
|
48
|
+
const [poolDeposits, orders, feeRate, accounts] = await Promise.all([
|
|
49
|
+
this.ickbLogic.findDeposits(client, tip),
|
|
50
|
+
this.order.findOrders(client),
|
|
51
|
+
getFeeRate(client),
|
|
52
|
+
Promise.all([...unique(locks)].map(async (lock) => this.readLockCells(client, lock, tip))),
|
|
53
|
+
]);
|
|
54
|
+
return {
|
|
55
|
+
system: {
|
|
56
|
+
feeRate,
|
|
57
|
+
tip,
|
|
58
|
+
exchangeRatio,
|
|
59
|
+
// One book, two filters: the market side is every order
|
|
60
|
+
// past par, the wallet's own included, since the bot fills by price, not by owner.
|
|
61
|
+
orderPool: orders.filter((group) => isPastPar(group, exchangeRatio)),
|
|
62
|
+
poolDeposits,
|
|
63
|
+
lockUp,
|
|
64
|
+
},
|
|
65
|
+
user: { orders: orders.filter((group) => group.isOwner(...locks)) },
|
|
66
|
+
account: {
|
|
67
|
+
capacityCells: accounts.flatMap((account) => account.capacityCells),
|
|
68
|
+
nativeUdtCells: accounts.flatMap((account) => account.nativeUdtCells),
|
|
69
|
+
receipts: accounts.flatMap((account) => account.receipts),
|
|
70
|
+
withdrawalGroups: accounts.flatMap((account) => account.withdrawalGroups),
|
|
71
|
+
},
|
|
49
72
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
}
|
|
74
|
+
/** Every Stack cell one lock owns, classified from a single exact-lock scan. */
|
|
75
|
+
async readLockCells(client, lock, tip) {
|
|
76
|
+
const cells = await findCells(client, {
|
|
77
|
+
script: lock,
|
|
78
|
+
scriptType: "lock",
|
|
79
|
+
scriptSearchMode: "exact",
|
|
80
|
+
withData: true,
|
|
81
|
+
});
|
|
82
|
+
const [receipts, withdrawalGroups] = await Promise.all([
|
|
83
|
+
this.ickbLogic.receiptsFrom(client, cells),
|
|
84
|
+
this.ownedOwner.withdrawalGroupsFrom(client, cells, tip),
|
|
85
|
+
]);
|
|
86
|
+
return {
|
|
87
|
+
capacityCells: cells.filter(isPlainCapacityCell),
|
|
88
|
+
nativeUdtCells: cells.filter((cell) => this.ickbUdt.isUdt(cell)),
|
|
89
|
+
receipts,
|
|
90
|
+
withdrawalGroups,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// ---- Conversion planning -----------------------------------------------------------
|
|
94
|
+
/**
|
|
95
|
+
* Builds and completes one conversion transaction from a conversion context.
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* Every branch returns a transaction funded from the signer's committed cells: the
|
|
99
|
+
* candidate plans (deposit counts, or prefixes of the greedy withdrawal selection with
|
|
100
|
+
* their rebuilt remainder order) are completed most direct first and the first fundable one
|
|
101
|
+
* wins, so a wallet short of CKB degrades to fewer direct actions plus a larger standing
|
|
102
|
+
* order rather than failing. Failure results are
|
|
103
|
+
* expected planning outcomes; when no plan can be funded the last completion error throws.
|
|
104
|
+
*/
|
|
105
|
+
async buildConversionTransaction(txLike, options) {
|
|
106
|
+
const { amount, context, direction } = options;
|
|
107
|
+
if (amount < 0n) {
|
|
108
|
+
return conversionFailure("amount-negative", context.estimatedMaturity);
|
|
63
109
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const ratio = isCkb2Udt ? info.ckbToUdt : info.udtToCkb;
|
|
67
|
-
if (amount === 0n) {
|
|
68
|
-
return 0n;
|
|
69
|
-
}
|
|
70
|
-
const { tip, exchangeRatio, orderPool, ckbAvailable, ckbMaturing } = system;
|
|
71
|
-
const b = new Info(ratio, ratio, 1);
|
|
72
|
-
let ckb = isCkb2Udt ? amount : 0n;
|
|
73
|
-
let udt = isCkb2Udt ? 0n : amount;
|
|
74
|
-
for (const o of orderPool) {
|
|
75
|
-
const a = o.data.info;
|
|
76
|
-
if (a.isCkb2Udt()) {
|
|
77
|
-
if (!isCkb2Udt || a.ckb2UdtCompare(b) < 0) {
|
|
78
|
-
ckb += o.ckbUnoccupied;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
if (isCkb2Udt || a.udt2CkbCompare(b) < 0) {
|
|
83
|
-
udt += o.udtValue;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
110
|
+
if (direction === "ckb-to-ickb" && amount > context.ckbAvailable) {
|
|
111
|
+
return conversionFailure("insufficient-ckb", context.estimatedMaturity);
|
|
86
112
|
}
|
|
87
|
-
ckb
|
|
88
|
-
|
|
89
|
-
if (isCkb2Udt) {
|
|
90
|
-
if (ckb > 0n) {
|
|
91
|
-
maturity *= 1n + ckb / ccc.fixedPointFrom("200000");
|
|
92
|
-
}
|
|
93
|
-
return maturity + ("info" in o ? BigInt(Date.now()) : tip.timestamp);
|
|
113
|
+
if (direction === "ickb-to-ckb" && amount > context.ickbAvailable) {
|
|
114
|
+
return conversionFailure("insufficient-ickb", context.estimatedMaturity);
|
|
94
115
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
const baseTx = ccc.Transaction.from(txLike);
|
|
117
|
+
const resolved = {
|
|
118
|
+
...options,
|
|
119
|
+
lock: options.lock ?? (await options.signer.getRecommendedAddressObj()).script,
|
|
120
|
+
};
|
|
121
|
+
if (amount === 0n) {
|
|
122
|
+
return this.buildCollectOnlyConversion(baseTx, resolved);
|
|
98
123
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
async request(tx, user, info, amounts) {
|
|
104
|
-
user =
|
|
105
|
-
"codeHash" in user
|
|
106
|
-
? user
|
|
107
|
-
: (await user.getRecommendedAddressObj()).script;
|
|
108
|
-
this.order.mint(tx, user, info, amounts);
|
|
124
|
+
return direction === "ckb-to-ickb"
|
|
125
|
+
? this.buildCkbToIckbConversion(baseTx, resolved)
|
|
126
|
+
: this.buildIckbToCkbConversion(baseTx, resolved);
|
|
109
127
|
}
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
async buildCollectOnlyConversion(baseTx, options) {
|
|
129
|
+
const { context, lock, signer } = options;
|
|
130
|
+
const tx = this.buildBaseTransaction(baseTx, context);
|
|
131
|
+
// An empty collection to a lock of the signer's own would only compact the account;
|
|
132
|
+
// to another lock it moves the liquid cells, which completion builds from the sweep.
|
|
133
|
+
if (!hasTransactionActivity(tx) &&
|
|
134
|
+
(context.cells.length === 0 || (await isOwnLock(signer, lock)))) {
|
|
135
|
+
return conversionFailure("nothing-to-do", context.estimatedMaturity);
|
|
136
|
+
}
|
|
137
|
+
const completed = await this.completeConversion(tx, options);
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
tx: completed,
|
|
141
|
+
estimatedMaturity: context.estimatedMaturity,
|
|
142
|
+
conversion: { kind: "collect-only" },
|
|
143
|
+
isSweepComplete: sweepsAll(completed, context.cells),
|
|
144
|
+
};
|
|
112
145
|
}
|
|
113
|
-
async
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
for (const group of orders) {
|
|
125
|
-
if (group.isOwner(...locks)) {
|
|
126
|
-
userOrders.push(group);
|
|
146
|
+
async buildCkbToIckbConversion(baseTx, options) {
|
|
147
|
+
const { context, lock } = options;
|
|
148
|
+
// A plan is dropped only for an unrepresentable remainder order.
|
|
149
|
+
const plans = ckbToIckbConversionPlans(options);
|
|
150
|
+
if (plans.length === 0) {
|
|
151
|
+
return conversionFailure("amount-too-small", context.estimatedMaturity, minimumOrderAmount(true, context.system));
|
|
152
|
+
}
|
|
153
|
+
const { candidate: plan, tx } = await completeFirstFundable(plans, (candidate) => {
|
|
154
|
+
let partial = this.buildBaseTransaction(baseTx.clone(), context);
|
|
155
|
+
if (candidate.depositCount > 0) {
|
|
156
|
+
partial = this.ickbLogic.deposit(partial, candidate.depositCount, candidate.depositCapacity, lock);
|
|
127
157
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if ((order.isCkb2UdtMatchable() && info.ckb2UdtCompare(midInfo) < 0) ||
|
|
131
|
-
(order.isUdt2CkbMatchable() && info.udt2CkbCompare(midInfo) < 0)) {
|
|
132
|
-
systemOrders.push(order);
|
|
158
|
+
if (candidate.order !== undefined) {
|
|
159
|
+
partial = this.order.mint(partial, lock, candidate.order.estimate.info, candidate.order.amounts);
|
|
133
160
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
161
|
+
return partial;
|
|
162
|
+
}, async (partial) => this.completeConversion(partial, options));
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
tx,
|
|
166
|
+
estimatedMaturity: plan.estimatedMaturity,
|
|
167
|
+
conversion: {
|
|
168
|
+
kind: conversionKind(plan.depositCount > 0, plan.order !== undefined),
|
|
169
|
+
},
|
|
170
|
+
isSweepComplete: sweepsAll(tx, context.cells),
|
|
142
171
|
};
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
}
|
|
173
|
+
async buildIckbToCkbConversion(baseTx, options) {
|
|
174
|
+
const { context, lock } = options;
|
|
175
|
+
const plans = ickbToCkbConversionPlans(options, context.system.poolDeposits);
|
|
176
|
+
if (plans.length === 0) {
|
|
177
|
+
return conversionFailure("amount-too-small", context.estimatedMaturity, minimumOrderAmount(false, context.system));
|
|
145
178
|
}
|
|
179
|
+
const { candidate: plan, tx } = await completeFirstFundable(plans, (candidate) => {
|
|
180
|
+
let partial = this.buildBaseTransaction(baseTx.clone(), context, {
|
|
181
|
+
deposits: candidate.selectedDeposits,
|
|
182
|
+
lock,
|
|
183
|
+
});
|
|
184
|
+
if (candidate.order !== undefined) {
|
|
185
|
+
partial = this.order.mint(partial, lock, candidate.order.estimate.info, candidate.order.amounts);
|
|
186
|
+
}
|
|
187
|
+
return partial;
|
|
188
|
+
}, async (partial) => this.completeConversion(partial, options));
|
|
189
|
+
const notice = plan.order?.estimate.notice;
|
|
190
|
+
const broadcastBefore = broadcastDeadline(plan.selectedDeposits.map((deposit) => deposit.claimEpoch), context.system.lockUp);
|
|
146
191
|
return {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
192
|
+
ok: true,
|
|
193
|
+
tx,
|
|
194
|
+
estimatedMaturity: plan.estimatedMaturity,
|
|
195
|
+
conversion: {
|
|
196
|
+
kind: conversionKind(plan.selectedDeposits.length > 0, plan.order !== undefined),
|
|
150
197
|
},
|
|
198
|
+
...(notice === undefined ? {} : { conversionNotice: notice }),
|
|
199
|
+
isSweepComplete: sweepsAll(tx, context.cells),
|
|
200
|
+
...(broadcastBefore === undefined ? {} : { broadcastBefore }),
|
|
151
201
|
};
|
|
152
202
|
}
|
|
153
|
-
async
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
203
|
+
async completeConversion(tx, options) {
|
|
204
|
+
return this.completeTransaction(tx, {
|
|
205
|
+
signer: options.signer,
|
|
206
|
+
lock: options.lock,
|
|
207
|
+
feeRate: options.context.system.feeRate,
|
|
208
|
+
cells: options.context.cells,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// ---- Transaction building ----------------------------------------------------------
|
|
212
|
+
/**
|
|
213
|
+
* Adds the context's collect steps to a partial transaction: the withdrawal requests when
|
|
214
|
+
* given, then the collectable orders, the receipts, and the ready withdrawals.
|
|
215
|
+
*
|
|
216
|
+
* @remarks
|
|
217
|
+
* The result is still partial. Callers should use `completeTransaction` before
|
|
218
|
+
* signing and sending. The withdrawals move their deposit headers to the front of
|
|
219
|
+
* `headerDeps`, so the given transaction must not reference a header by position.
|
|
220
|
+
*/
|
|
221
|
+
buildBaseTransaction(txLike, steps, withdrawalRequest) {
|
|
222
|
+
let tx = ccc.Transaction.from(txLike);
|
|
223
|
+
if (withdrawalRequest !== undefined && withdrawalRequest.deposits.length > 0) {
|
|
224
|
+
tx = this.ownedOwner.requestWithdrawal(tx, withdrawalRequest.deposits, withdrawalRequest.lock);
|
|
225
|
+
}
|
|
226
|
+
if (steps.availableOrders.length > 0) {
|
|
227
|
+
tx = this.order.melt(tx, steps.availableOrders);
|
|
228
|
+
}
|
|
229
|
+
if (steps.receipts.length > 0) {
|
|
230
|
+
tx = this.ickbLogic.completeDeposit(tx, steps.receipts);
|
|
231
|
+
}
|
|
232
|
+
if (steps.readyWithdrawals.length > 0) {
|
|
233
|
+
tx = this.ownedOwner.withdraw(tx, steps.readyWithdrawals);
|
|
234
|
+
}
|
|
235
|
+
return tx;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Completes iCKB inputs, iCKB change, plain change, and the fee from the cells it is given.
|
|
239
|
+
*
|
|
240
|
+
* @remarks Completion never scans. `cells` are the signer's known liquid cells, plain
|
|
241
|
+
* CKB and iCKB, from the account state already read: the largest ones fund what the
|
|
242
|
+
* outputs need, the rest ride along as a sweep while the transaction has fewer than
|
|
243
|
+
* {@link SWEEP_INPUT_LIMIT} inputs, so every own transaction compacts the account.
|
|
244
|
+
* Ordinary change is always a plain cell; existing outputs
|
|
245
|
+
* are never reinterpreted or resized as fee change. This does not sign or send.
|
|
246
|
+
*/
|
|
247
|
+
async completeTransaction(txLike, options) {
|
|
248
|
+
const { signer, feeRate } = options;
|
|
249
|
+
const tx = this.ickbUdt.addCellDeps(ccc.Transaction.from(txLike).clone());
|
|
250
|
+
const changeLock = options.lock ?? (await signer.getRecommendedAddressObj()).script;
|
|
251
|
+
// Inputs come from one state read, each cell once, and each action spends its own
|
|
252
|
+
// cell kind, so a repeated out point is a builder bug: this is the one place that
|
|
253
|
+
// names it, the node would refuse the transaction anyway.
|
|
254
|
+
const spent = new Set();
|
|
255
|
+
for (const { previousOutput } of tx.inputs) {
|
|
256
|
+
const key = previousOutput.toHex();
|
|
257
|
+
if (spent.has(key)) {
|
|
258
|
+
throw new Error(`Input ${key} is spent twice`);
|
|
178
259
|
}
|
|
260
|
+
spent.add(key);
|
|
179
261
|
}
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
262
|
+
const unspent = options.cells.filter((cell) => !spent.has(cell.outPoint.toHex()));
|
|
263
|
+
const ickbCells = unspent
|
|
264
|
+
.filter((cell) => this.ickbUdt.isUdt(cell))
|
|
265
|
+
.toSorted((left, right) => compareBigInt(udtBalance(right), udtBalance(left)));
|
|
266
|
+
const plainCells = unspent
|
|
267
|
+
.filter(isPlainCapacityCell)
|
|
268
|
+
.toSorted((left, right) => compareBigInt(right.cellOutput.capacity, left.cellOutput.capacity));
|
|
269
|
+
await this.completeIckb(tx, signer.client, changeLock, ickbCells);
|
|
270
|
+
// Plain CKB: the sweep first, then whatever the fee still needs beyond the limit.
|
|
271
|
+
const swept = sweep(tx, plainCells);
|
|
272
|
+
await completeFee(tx, signer, changeLock, feeRate, plainCells.slice(swept));
|
|
273
|
+
assertDaoOutputLimit(tx, this.ickbLogic.dao.script);
|
|
274
|
+
return tx;
|
|
275
|
+
}
|
|
276
|
+
/** iCKB: what the outputs need first, then the sweep while the limit allows, then change. */
|
|
277
|
+
async completeIckb(tx, client, changeLock, ickbCells) {
|
|
278
|
+
const required = this.ickbUdt.outputBalance(tx);
|
|
279
|
+
let balance = await this.ickbUdt.inputBalance(tx, client);
|
|
280
|
+
for (const cell of ickbCells) {
|
|
281
|
+
if (balance >= required && tx.inputs.length >= SWEEP_INPUT_LIMIT) {
|
|
282
|
+
break;
|
|
187
283
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
284
|
+
tx.addInput(cell);
|
|
285
|
+
balance += udtBalance(cell);
|
|
286
|
+
}
|
|
287
|
+
if (balance < required) {
|
|
288
|
+
throw new IckbError(`Insufficient iCKB, need ${String(required - balance)} more`, {
|
|
289
|
+
code: "insufficient_ickb",
|
|
191
290
|
});
|
|
192
291
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
292
|
+
this.ickbUdt.addChange(tx, changeLock, balance - required);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
/** Concrete iCKB SDK constructor and static estimators. */
|
|
296
|
+
// eslint-disable-next-line @typescript-eslint/no-redeclare -- The public type and runtime constructor intentionally share a name.
|
|
297
|
+
export const IckbSdk = IckbSdkImplementation;
|
|
298
|
+
/** Whether the transaction already carries any input or output. */
|
|
299
|
+
export function hasTransactionActivity(tx) {
|
|
300
|
+
return tx.inputs.length > 0 || tx.outputs.length > 0;
|
|
301
|
+
}
|
|
302
|
+
/** An order the market can fill at a gain: priced past the DAO ratio on its matchable side. */
|
|
303
|
+
function isPastPar(group, exchangeRatio) {
|
|
304
|
+
const { order } = group;
|
|
305
|
+
const { info } = order.data;
|
|
306
|
+
return ((order.isCkb2UdtMatchable() && info.ckbToUdt.compare(exchangeRatio) < 0) ||
|
|
307
|
+
(order.isUdt2CkbMatchable() && exchangeRatio.compare(info.udtToCkb) < 0));
|
|
308
|
+
}
|
|
309
|
+
function conversionFailure(reason, estimatedMaturity, minimum) {
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
reason,
|
|
313
|
+
estimatedMaturity,
|
|
314
|
+
...(minimum === undefined ? {} : { minimum }),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** A plan always carries a deposit or an order, else it would be a collection. */
|
|
318
|
+
function conversionKind(hasDirect, hasOrder) {
|
|
319
|
+
if (hasDirect && hasOrder) {
|
|
320
|
+
return "direct-plus-order";
|
|
321
|
+
}
|
|
322
|
+
return hasDirect ? "direct" : "order";
|
|
323
|
+
}
|
|
324
|
+
/** Whether every liquid cell became an input; false when the input limit cut the sweep. */
|
|
325
|
+
function sweepsAll(tx, cells) {
|
|
326
|
+
const spent = new Set(tx.inputs.map((input) => input.previousOutput.toHex()));
|
|
327
|
+
return cells.every((cell) => spent.has(cell.outPoint.toHex()));
|
|
328
|
+
}
|
|
329
|
+
async function isOwnLock(signer, lock) {
|
|
330
|
+
return (await signer.getAddressObjs()).some(({ script }) => script.eq(lock));
|
|
331
|
+
}
|
|
332
|
+
async function getFeeRate(client) {
|
|
333
|
+
const feeRate = await client.getFeeRate();
|
|
334
|
+
if (feeRate < 0n) {
|
|
335
|
+
throw new Error("Client fee rate must be non-negative");
|
|
336
|
+
}
|
|
337
|
+
return feeRate;
|
|
338
|
+
}
|
|
339
|
+
function udtBalance(cell) {
|
|
340
|
+
return ccc.udtBalanceFrom(cell.outputData);
|
|
341
|
+
}
|
|
342
|
+
/** Adds cells while the input count stays under the limit; returns how many were added. */
|
|
343
|
+
function sweep(tx, cells) {
|
|
344
|
+
const added = Math.min(cells.length, Math.max(0, SWEEP_INPUT_LIMIT - tx.inputs.length));
|
|
345
|
+
for (const cell of cells.slice(0, added)) {
|
|
346
|
+
tx.addInput(cell);
|
|
347
|
+
}
|
|
348
|
+
return added;
|
|
349
|
+
}
|
|
350
|
+
/** Completes the fee, adding reserve cells one at a time while capacity is short. */
|
|
351
|
+
async function completeFee(tx, signer, changeLock, feeRate, reserve) {
|
|
352
|
+
const remaining = [...reserve];
|
|
353
|
+
for (;;) {
|
|
354
|
+
try {
|
|
355
|
+
await tx.completeFeeChangeToLock(signer, changeLock, feeRate, undefined, {
|
|
356
|
+
shouldAddInputs: false,
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
198
359
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const eNumber = tip.epoch[0];
|
|
203
|
-
let start = Epoch.from([eNumber - (eNumber % 180n), 0n, 1n]);
|
|
204
|
-
const step = Epoch.from([0n, 180n, 1024n]);
|
|
205
|
-
const depositSize = convert(false, ICKB_DEPOSIT_CAP, tip);
|
|
206
|
-
for (const binAmount of PoolSnapshot.decode(poolSnapshotHex)) {
|
|
207
|
-
const end = start.add(step);
|
|
208
|
-
if (binAmount > 0) {
|
|
209
|
-
ckbMaturing.push({
|
|
210
|
-
ckbValue: BigInt(binAmount) * depositSize,
|
|
211
|
-
maturity: tipEpoch.compare(tipEpoch) < 0
|
|
212
|
-
?
|
|
213
|
-
end.add(oneCycle).toUnix(tip)
|
|
214
|
-
:
|
|
215
|
-
end.toUnix(tip),
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
start = end;
|
|
360
|
+
catch (error) {
|
|
361
|
+
if (!(error instanceof ccc.ErrorTransactionInsufficientCapacity)) {
|
|
362
|
+
throw error;
|
|
219
363
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
maturity: d.maturity.toUnix(tip),
|
|
364
|
+
const cell = remaining.shift();
|
|
365
|
+
if (cell === undefined) {
|
|
366
|
+
throw new IckbError(error.message, {
|
|
367
|
+
code: "insufficient_capacity",
|
|
368
|
+
cause: error,
|
|
226
369
|
});
|
|
227
370
|
}
|
|
371
|
+
tx.addInput(cell);
|
|
228
372
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** Typed SDK failure with a stable machine-readable code. */
|
|
376
|
+
export class IckbError extends Error {
|
|
377
|
+
/** Stable failure code for callers and observability. */
|
|
378
|
+
code;
|
|
379
|
+
/** Creates a typed SDK failure while preserving its cause. */
|
|
380
|
+
constructor(message, options) {
|
|
381
|
+
super(message, options);
|
|
382
|
+
this.name = "IckbError";
|
|
383
|
+
this.code = options.code;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Completes the first candidate that the real completer can fund.
|
|
388
|
+
*
|
|
389
|
+
* @remarks
|
|
390
|
+
* Only completion knows what a transaction costs once markers, remainder orders, change, and
|
|
391
|
+
* fee are in, so no count is computed up front: each candidate is built and completed in turn.
|
|
392
|
+
* Capacity, DAO output-limit and DAO header-index failures advance
|
|
393
|
+
* the walk; transport, scan, signer, and malformed-transaction errors propagate. Exhausting
|
|
394
|
+
* the candidates throws the last advancing failure.
|
|
395
|
+
*/
|
|
396
|
+
export async function completeFirstFundable(candidates, build, complete) {
|
|
397
|
+
let error;
|
|
398
|
+
for (const candidate of candidates) {
|
|
399
|
+
try {
|
|
400
|
+
return { candidate, tx: await complete(build(candidate)) };
|
|
401
|
+
}
|
|
402
|
+
catch (failure) {
|
|
403
|
+
if (!isFundabilityFailure(failure)) {
|
|
404
|
+
throw failure;
|
|
405
|
+
}
|
|
406
|
+
error = failure;
|
|
235
407
|
}
|
|
236
|
-
return {
|
|
237
|
-
ckbAvailable,
|
|
238
|
-
ckbMaturing: ckbCumulativeMaturing,
|
|
239
|
-
};
|
|
240
408
|
}
|
|
409
|
+
throw error ?? new Error("No candidate could be completed");
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* A failure the walk advances past: the candidate costs more than the account funds, or
|
|
413
|
+
* needs more DAO outputs or deposit-header slots than one transaction has.
|
|
414
|
+
*/
|
|
415
|
+
export function isFundabilityFailure(error) {
|
|
416
|
+
return (error instanceof IckbError ||
|
|
417
|
+
error instanceof ccc.ErrorTransactionInsufficientCapacity ||
|
|
418
|
+
error instanceof DaoOutputLimitError ||
|
|
419
|
+
error instanceof DaoHeaderIndexError);
|
|
241
420
|
}
|
|
242
421
|
//# sourceMappingURL=sdk.js.map
|