@sequence0/sdk 2.0.1 → 2.1.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 +10 -10
- package/dist/chains/casper.d.ts +74 -0
- package/dist/chains/casper.d.ts.map +1 -0
- package/dist/chains/casper.js +512 -0
- package/dist/chains/casper.js.map +1 -0
- package/dist/chains/cosmos.d.ts +22 -0
- package/dist/chains/cosmos.d.ts.map +1 -1
- package/dist/chains/cosmos.js +113 -12
- package/dist/chains/cosmos.js.map +1 -1
- package/dist/chains/ethereum.d.ts.map +1 -1
- package/dist/chains/ethereum.js +14 -2
- package/dist/chains/ethereum.js.map +1 -1
- package/dist/chains/flow.d.ts +57 -0
- package/dist/chains/flow.d.ts.map +1 -0
- package/dist/chains/flow.js +435 -0
- package/dist/chains/flow.js.map +1 -0
- package/dist/chains/icp.d.ts.map +1 -1
- package/dist/chains/icp.js +483 -67
- package/dist/chains/icp.js.map +1 -1
- package/dist/chains/iota.d.ts +80 -0
- package/dist/chains/iota.d.ts.map +1 -0
- package/dist/chains/iota.js +502 -0
- package/dist/chains/iota.js.map +1 -0
- package/dist/chains/kadena.d.ts +81 -0
- package/dist/chains/kadena.d.ts.map +1 -0
- package/dist/chains/kadena.js +356 -0
- package/dist/chains/kadena.js.map +1 -0
- package/dist/chains/near.d.ts +4 -1
- package/dist/chains/near.d.ts.map +1 -1
- package/dist/chains/near.js +58 -15
- package/dist/chains/near.js.map +1 -1
- package/dist/chains/nervos.d.ts +148 -0
- package/dist/chains/nervos.d.ts.map +1 -0
- package/dist/chains/nervos.js +913 -0
- package/dist/chains/nervos.js.map +1 -0
- package/dist/chains/radix.d.ts +81 -0
- package/dist/chains/radix.d.ts.map +1 -0
- package/dist/chains/radix.js +289 -0
- package/dist/chains/radix.js.map +1 -0
- package/dist/chains/solana.d.ts +4 -0
- package/dist/chains/solana.d.ts.map +1 -1
- package/dist/chains/solana.js +47 -13
- package/dist/chains/solana.js.map +1 -1
- package/dist/chains/stacks.d.ts +113 -0
- package/dist/chains/stacks.d.ts.map +1 -0
- package/dist/chains/stacks.js +576 -0
- package/dist/chains/stacks.js.map +1 -0
- package/dist/chains/sui.d.ts +11 -0
- package/dist/chains/sui.d.ts.map +1 -1
- package/dist/chains/sui.js +49 -8
- package/dist/chains/sui.js.map +1 -1
- package/dist/core/client.js +1 -1
- package/dist/core/client.js.map +1 -1
- package/dist/core/solvency.d.ts +1 -1
- package/dist/core/solvency.js +1 -1
- package/dist/core/types.d.ts +94 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/universal-account.d.ts +1 -1
- package/dist/core/universal-account.js +1 -1
- package/dist/core/witness.d.ts +1 -1
- package/dist/core/witness.js +1 -1
- package/dist/settlement/settlement.d.ts +1 -1
- package/dist/settlement/settlement.js +1 -1
- package/dist/utils/discovery.d.ts.map +1 -1
- package/dist/utils/discovery.js +51 -4
- package/dist/utils/discovery.js.map +1 -1
- package/dist/utils/http.d.ts +1 -1
- package/dist/utils/http.js +1 -1
- package/dist/wallet/wallet.d.ts +11 -1
- package/dist/wallet/wallet.d.ts.map +1 -1
- package/dist/wallet/wallet.js +60 -2
- package/dist/wallet/wallet.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Nervos CKB (Common Knowledge Base) Chain Adapter
|
|
4
|
+
*
|
|
5
|
+
* Builds CKB transactions using the Cell model, computes signing payloads
|
|
6
|
+
* with Blake2b-256 hashing, attaches secp256k1 ECDSA signatures from the
|
|
7
|
+
* FROST threshold signing network, and broadcasts via CKB JSON-RPC.
|
|
8
|
+
*
|
|
9
|
+
* Nervos CKB uses:
|
|
10
|
+
* - Cell model (generalized UTXO) instead of account-based state
|
|
11
|
+
* - Blake2b-256 for transaction hashing (with CKB-specific personalization)
|
|
12
|
+
* - secp256k1 ECDSA with 65-byte recoverable signatures (r + s + recovery_id)
|
|
13
|
+
* - Molecule binary serialization for on-chain data structures
|
|
14
|
+
*
|
|
15
|
+
* No external dependencies beyond @noble/hashes/blake2b (transitive dep).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { NervosAdapter } from '@sequence0/sdk';
|
|
20
|
+
*
|
|
21
|
+
* const ckb = new NervosAdapter('mainnet');
|
|
22
|
+
*
|
|
23
|
+
* // Build a CKB transfer transaction
|
|
24
|
+
* const unsignedTx = await ckb.buildTransaction(
|
|
25
|
+
* { to: 'ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq...', amount: '10000000000' },
|
|
26
|
+
* 'ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq...'
|
|
27
|
+
* );
|
|
28
|
+
*
|
|
29
|
+
* // ... sign via FROST secp256k1 ...
|
|
30
|
+
* const signedTx = await ckb.attachSignature(unsignedTx, signatureHex);
|
|
31
|
+
* const txHash = await ckb.broadcast(signedTx);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.NervosAdapter = void 0;
|
|
36
|
+
exports.createNervosAdapter = createNervosAdapter;
|
|
37
|
+
exports.createNervosTestnetAdapter = createNervosTestnetAdapter;
|
|
38
|
+
const errors_1 = require("../utils/errors");
|
|
39
|
+
const blake2b_1 = require("@noble/hashes/blake2b");
|
|
40
|
+
// ── Network Configuration ──
|
|
41
|
+
const DEFAULT_RPC_URLS = {
|
|
42
|
+
'mainnet': 'https://mainnet.ckb.dev/rpc',
|
|
43
|
+
'testnet': 'https://testnet.ckb.dev/rpc',
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* CKB personalization string for Blake2b hashing.
|
|
47
|
+
* CKB uses "ckb-default-hash" as the personalization parameter
|
|
48
|
+
* for all Blake2b-256 hashes in the protocol.
|
|
49
|
+
*/
|
|
50
|
+
const CKB_HASH_PERSONALIZATION = 'ckb-default-hash';
|
|
51
|
+
/** Minimum cell capacity in shannons (61 CKB = 6100000000 shannons for a basic cell) */
|
|
52
|
+
const MIN_CELL_CAPACITY = BigInt(6100000000);
|
|
53
|
+
/** Shannon per CKB (1 CKB = 1e8 shannons) */
|
|
54
|
+
const SHANNONS_PER_CKB = BigInt(100000000);
|
|
55
|
+
/**
|
|
56
|
+
* Default secp256k1_blake160 cell dep for mainnet (genesis block dep group).
|
|
57
|
+
* This references the system script cell dep group in the genesis block.
|
|
58
|
+
*/
|
|
59
|
+
const SECP256K1_BLAKE160_CELL_DEPS = {
|
|
60
|
+
'mainnet': {
|
|
61
|
+
outPoint: {
|
|
62
|
+
txHash: '0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c',
|
|
63
|
+
index: '0x0',
|
|
64
|
+
},
|
|
65
|
+
depType: 'dep_group',
|
|
66
|
+
},
|
|
67
|
+
'testnet': {
|
|
68
|
+
outPoint: {
|
|
69
|
+
txHash: '0xf8de3bb47d055cdf460d93a2a6e1b05f7432f9777c8c474abf4eec1d4aee5d37',
|
|
70
|
+
index: '0x0',
|
|
71
|
+
},
|
|
72
|
+
depType: 'dep_group',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* secp256k1_blake160 lock script code hash (type ID).
|
|
77
|
+
* This is the same on both mainnet and testnet.
|
|
78
|
+
*/
|
|
79
|
+
const SECP256K1_BLAKE160_CODE_HASH = '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8';
|
|
80
|
+
// ── Blake2b-256 with CKB Personalization ──
|
|
81
|
+
/**
|
|
82
|
+
* Compute Blake2b-256 with the CKB-specific personalization string.
|
|
83
|
+
*
|
|
84
|
+
* CKB uses Blake2b with:
|
|
85
|
+
* - 32-byte output (256 bits)
|
|
86
|
+
* - personalization: "ckb-default-hash" (UTF-8 encoded, zero-padded to 16 bytes)
|
|
87
|
+
*
|
|
88
|
+
* @param data - Input data as Uint8Array
|
|
89
|
+
* @returns 32-byte Blake2b-256 hash
|
|
90
|
+
*/
|
|
91
|
+
function ckbBlake2b(data) {
|
|
92
|
+
return (0, blake2b_1.blake2b)(data, {
|
|
93
|
+
dkLen: 32,
|
|
94
|
+
personalization: CKB_HASH_PERSONALIZATION,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Compute Blake2b-256 hash incrementally for multiple data chunks.
|
|
99
|
+
*
|
|
100
|
+
* @param chunks - Array of Uint8Array data chunks
|
|
101
|
+
* @returns 32-byte Blake2b-256 hash
|
|
102
|
+
*/
|
|
103
|
+
function ckbBlake2bMulti(...chunks) {
|
|
104
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
105
|
+
const combined = new Uint8Array(totalLength);
|
|
106
|
+
let offset = 0;
|
|
107
|
+
for (const chunk of chunks) {
|
|
108
|
+
combined.set(chunk, offset);
|
|
109
|
+
offset += chunk.length;
|
|
110
|
+
}
|
|
111
|
+
return ckbBlake2b(combined);
|
|
112
|
+
}
|
|
113
|
+
// ── Address Encoding/Decoding ──
|
|
114
|
+
const BECH32M_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
115
|
+
const BECH32M_CONST = 0x2bc830a3;
|
|
116
|
+
function bech32mPolymod(values) {
|
|
117
|
+
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
118
|
+
let chk = 1;
|
|
119
|
+
for (const v of values) {
|
|
120
|
+
const b = chk >> 25;
|
|
121
|
+
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
|
122
|
+
for (let i = 0; i < 5; i++) {
|
|
123
|
+
if ((b >> i) & 1) {
|
|
124
|
+
chk ^= GEN[i];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return chk;
|
|
129
|
+
}
|
|
130
|
+
function bech32mHrpExpand(hrp) {
|
|
131
|
+
const ret = [];
|
|
132
|
+
for (const c of hrp) {
|
|
133
|
+
ret.push(c.charCodeAt(0) >> 5);
|
|
134
|
+
}
|
|
135
|
+
ret.push(0);
|
|
136
|
+
for (const c of hrp) {
|
|
137
|
+
ret.push(c.charCodeAt(0) & 31);
|
|
138
|
+
}
|
|
139
|
+
return ret;
|
|
140
|
+
}
|
|
141
|
+
function bech32mVerify(hrp, data5bit) {
|
|
142
|
+
return bech32mPolymod(bech32mHrpExpand(hrp).concat(data5bit)) === BECH32M_CONST;
|
|
143
|
+
}
|
|
144
|
+
function convertBits(data, fromBits, toBits, pad) {
|
|
145
|
+
let acc = 0;
|
|
146
|
+
let bits = 0;
|
|
147
|
+
const ret = [];
|
|
148
|
+
const maxv = (1 << toBits) - 1;
|
|
149
|
+
for (const value of data) {
|
|
150
|
+
if (value < 0 || value >> fromBits !== 0) {
|
|
151
|
+
throw new Error('Invalid value for bit conversion');
|
|
152
|
+
}
|
|
153
|
+
acc = (acc << fromBits) | value;
|
|
154
|
+
bits += fromBits;
|
|
155
|
+
while (bits >= toBits) {
|
|
156
|
+
bits -= toBits;
|
|
157
|
+
ret.push((acc >> bits) & maxv);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (pad) {
|
|
161
|
+
if (bits > 0) {
|
|
162
|
+
ret.push((acc << (toBits - bits)) & maxv);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv)) {
|
|
166
|
+
throw new Error('Invalid padding in bech32m conversion');
|
|
167
|
+
}
|
|
168
|
+
return ret;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Decode a CKB full address (new format, CKB2021).
|
|
172
|
+
*
|
|
173
|
+
* CKB full addresses use bech32m encoding with format:
|
|
174
|
+
* - ckb1... (mainnet)
|
|
175
|
+
* - ckt1... (testnet)
|
|
176
|
+
*
|
|
177
|
+
* The payload encodes: format_type (0x00) + code_hash (32 bytes) + hash_type (1 byte) + args (20 bytes)
|
|
178
|
+
*
|
|
179
|
+
* For default secp256k1_blake160 lock:
|
|
180
|
+
* - code_hash: SECP256K1_BLAKE160_CODE_HASH
|
|
181
|
+
* - hash_type: 0x01 (type)
|
|
182
|
+
* - args: 20-byte blake160 hash of the public key
|
|
183
|
+
*/
|
|
184
|
+
function decodeCkbAddress(address) {
|
|
185
|
+
const parts = address.toLowerCase().split('1');
|
|
186
|
+
if (parts.length < 2) {
|
|
187
|
+
throw new Error(`Invalid CKB address format: ${address}`);
|
|
188
|
+
}
|
|
189
|
+
const hrp = parts[0];
|
|
190
|
+
if (hrp !== 'ckb' && hrp !== 'ckt') {
|
|
191
|
+
throw new Error(`Invalid CKB address prefix: ${hrp} (expected ckb or ckt)`);
|
|
192
|
+
}
|
|
193
|
+
// Everything after the first '1' is the data part
|
|
194
|
+
const dataStr = address.substring(hrp.length + 1).toLowerCase();
|
|
195
|
+
const data5bit = [];
|
|
196
|
+
for (const c of dataStr) {
|
|
197
|
+
const idx = BECH32M_CHARSET.indexOf(c);
|
|
198
|
+
if (idx < 0)
|
|
199
|
+
throw new Error(`Invalid character in CKB address: ${c}`);
|
|
200
|
+
data5bit.push(idx);
|
|
201
|
+
}
|
|
202
|
+
// Verify bech32m checksum
|
|
203
|
+
if (!bech32mVerify(hrp, data5bit)) {
|
|
204
|
+
throw new Error('Invalid CKB address checksum');
|
|
205
|
+
}
|
|
206
|
+
// Remove checksum (6 characters)
|
|
207
|
+
const payload5bit = data5bit.slice(0, data5bit.length - 6);
|
|
208
|
+
// Convert from 5-bit to 8-bit
|
|
209
|
+
const payload8bit = convertBits(payload5bit, 5, 8, false);
|
|
210
|
+
// CKB2021 full address format:
|
|
211
|
+
// payload[0] = 0x00 (full format type)
|
|
212
|
+
// payload[1..33] = code_hash (32 bytes)
|
|
213
|
+
// payload[33] = hash_type (0x00=data, 0x01=type, 0x02=data1, 0x04=data2)
|
|
214
|
+
// payload[34..] = args
|
|
215
|
+
if (payload8bit[0] !== 0x00) {
|
|
216
|
+
throw new Error(`Unsupported CKB address format type: 0x${payload8bit[0].toString(16)}`);
|
|
217
|
+
}
|
|
218
|
+
const codeHash = '0x' + Buffer.from(payload8bit.slice(1, 33)).toString('hex');
|
|
219
|
+
const hashTypeByte = payload8bit[33];
|
|
220
|
+
let hashType;
|
|
221
|
+
switch (hashTypeByte) {
|
|
222
|
+
case 0x00:
|
|
223
|
+
hashType = 'data';
|
|
224
|
+
break;
|
|
225
|
+
case 0x01:
|
|
226
|
+
hashType = 'type';
|
|
227
|
+
break;
|
|
228
|
+
case 0x02:
|
|
229
|
+
hashType = 'data1';
|
|
230
|
+
break;
|
|
231
|
+
case 0x04:
|
|
232
|
+
hashType = 'data2';
|
|
233
|
+
break;
|
|
234
|
+
default: throw new Error(`Unknown CKB hash type: 0x${hashTypeByte.toString(16)}`);
|
|
235
|
+
}
|
|
236
|
+
const args = '0x' + Buffer.from(payload8bit.slice(34)).toString('hex');
|
|
237
|
+
return { codeHash, hashType, args };
|
|
238
|
+
}
|
|
239
|
+
// ── Molecule Serialization ──
|
|
240
|
+
// CKB uses Molecule for binary serialization of transaction data.
|
|
241
|
+
// We implement the minimum needed for transaction hashing.
|
|
242
|
+
/**
|
|
243
|
+
* Serialize a byte array in Molecule format (fixed-size vector).
|
|
244
|
+
* For CKB scripts, Byte32 is just raw 32 bytes without length prefix.
|
|
245
|
+
*/
|
|
246
|
+
function hexToBytes(hex) {
|
|
247
|
+
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
248
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
249
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
250
|
+
bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
|
|
251
|
+
}
|
|
252
|
+
return bytes;
|
|
253
|
+
}
|
|
254
|
+
function bytesToHex(bytes) {
|
|
255
|
+
return '0x' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
256
|
+
}
|
|
257
|
+
/** Write a 32-bit little-endian unsigned integer */
|
|
258
|
+
function writeUint32LE(value) {
|
|
259
|
+
const buf = new Uint8Array(4);
|
|
260
|
+
buf[0] = value & 0xff;
|
|
261
|
+
buf[1] = (value >> 8) & 0xff;
|
|
262
|
+
buf[2] = (value >> 16) & 0xff;
|
|
263
|
+
buf[3] = (value >> 24) & 0xff;
|
|
264
|
+
return buf;
|
|
265
|
+
}
|
|
266
|
+
/** Write a 64-bit little-endian unsigned integer from a BigInt */
|
|
267
|
+
function writeUint64LE(value) {
|
|
268
|
+
const buf = new Uint8Array(8);
|
|
269
|
+
for (let i = 0; i < 8; i++) {
|
|
270
|
+
buf[i] = Number((value >> BigInt(i * 8)) & 0xffn);
|
|
271
|
+
}
|
|
272
|
+
return buf;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Serialize a Molecule fixvec (fixed-size element vector).
|
|
276
|
+
* Format: 4-byte LE length (item count) + concatenated items
|
|
277
|
+
*/
|
|
278
|
+
function serializeFixvec(items) {
|
|
279
|
+
const count = writeUint32LE(items.length);
|
|
280
|
+
const totalLen = 4 + items.reduce((sum, item) => sum + item.length, 0);
|
|
281
|
+
const result = new Uint8Array(totalLen);
|
|
282
|
+
result.set(count, 0);
|
|
283
|
+
let offset = 4;
|
|
284
|
+
for (const item of items) {
|
|
285
|
+
result.set(item, offset);
|
|
286
|
+
offset += item.length;
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Serialize a Molecule dynvec (dynamic-size element vector).
|
|
292
|
+
* Format: 4-byte LE total_size + 4-byte LE offsets per item + concatenated items
|
|
293
|
+
*/
|
|
294
|
+
function serializeDynvec(items) {
|
|
295
|
+
const headerSize = 4 + items.length * 4; // total_size + offsets
|
|
296
|
+
const totalSize = headerSize + items.reduce((sum, item) => sum + item.length, 0);
|
|
297
|
+
const result = new Uint8Array(totalSize);
|
|
298
|
+
// Write total size
|
|
299
|
+
result.set(writeUint32LE(totalSize), 0);
|
|
300
|
+
// Write offsets
|
|
301
|
+
let dataOffset = headerSize;
|
|
302
|
+
for (let i = 0; i < items.length; i++) {
|
|
303
|
+
result.set(writeUint32LE(dataOffset), 4 + i * 4);
|
|
304
|
+
dataOffset += items[i].length;
|
|
305
|
+
}
|
|
306
|
+
// Write items
|
|
307
|
+
dataOffset = headerSize;
|
|
308
|
+
for (const item of items) {
|
|
309
|
+
result.set(item, dataOffset);
|
|
310
|
+
dataOffset += item.length;
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Serialize a Molecule table (struct with dynamic fields).
|
|
316
|
+
* Same format as dynvec: 4-byte LE total_size + offsets + fields
|
|
317
|
+
*/
|
|
318
|
+
function serializeTable(fields) {
|
|
319
|
+
return serializeDynvec(fields);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Serialize a CKB Script in Molecule format.
|
|
323
|
+
* Script is a table: { code_hash: Byte32, hash_type: byte, args: Bytes }
|
|
324
|
+
*/
|
|
325
|
+
function serializeScript(script) {
|
|
326
|
+
const codeHash = hexToBytes(script.codeHash); // 32 bytes
|
|
327
|
+
let hashTypeByte;
|
|
328
|
+
switch (script.hashType) {
|
|
329
|
+
case 'data':
|
|
330
|
+
hashTypeByte = 0x00;
|
|
331
|
+
break;
|
|
332
|
+
case 'type':
|
|
333
|
+
hashTypeByte = 0x01;
|
|
334
|
+
break;
|
|
335
|
+
case 'data1':
|
|
336
|
+
hashTypeByte = 0x02;
|
|
337
|
+
break;
|
|
338
|
+
case 'data2':
|
|
339
|
+
hashTypeByte = 0x04;
|
|
340
|
+
break;
|
|
341
|
+
default: hashTypeByte = 0x00;
|
|
342
|
+
}
|
|
343
|
+
const hashType = new Uint8Array([hashTypeByte]);
|
|
344
|
+
// args is a Molecule Bytes (4-byte LE length + data)
|
|
345
|
+
const argsData = hexToBytes(script.args);
|
|
346
|
+
const argsBytes = new Uint8Array(4 + argsData.length);
|
|
347
|
+
argsBytes.set(writeUint32LE(argsData.length), 0);
|
|
348
|
+
argsBytes.set(argsData, 4);
|
|
349
|
+
return serializeTable([codeHash, hashType, argsBytes]);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Serialize a CellOutput in Molecule format.
|
|
353
|
+
* CellOutput is a table: { capacity: Uint64, lock: Script, type_: ScriptOpt }
|
|
354
|
+
*/
|
|
355
|
+
function serializeCellOutput(output) {
|
|
356
|
+
const capacity = writeUint64LE(BigInt(output.capacity.startsWith('0x')
|
|
357
|
+
? parseInt(output.capacity, 16)
|
|
358
|
+
: output.capacity));
|
|
359
|
+
const lock = serializeScript(output.lock);
|
|
360
|
+
// ScriptOpt: empty (0 bytes) if null, or serialized Script
|
|
361
|
+
const typeScript = output.type ? serializeScript(output.type) : new Uint8Array(0);
|
|
362
|
+
return serializeTable([capacity, lock, typeScript]);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Serialize a CellInput in Molecule format.
|
|
366
|
+
* CellInput is a struct: { since: Uint64, previous_output: OutPoint }
|
|
367
|
+
* OutPoint is a struct: { tx_hash: Byte32, index: Uint32 }
|
|
368
|
+
* Structs are fixed-size, just concatenated fields.
|
|
369
|
+
*/
|
|
370
|
+
function serializeCellInput(input) {
|
|
371
|
+
const since = writeUint64LE(BigInt(input.since.startsWith('0x')
|
|
372
|
+
? parseInt(input.since, 16)
|
|
373
|
+
: input.since));
|
|
374
|
+
const txHash = hexToBytes(input.previousOutput.txHash); // 32 bytes
|
|
375
|
+
const index = writeUint32LE(parseInt(input.previousOutput.index, 16));
|
|
376
|
+
// Struct: fields concatenated directly (no length prefix)
|
|
377
|
+
const result = new Uint8Array(8 + 32 + 4);
|
|
378
|
+
result.set(since, 0);
|
|
379
|
+
result.set(txHash, 8);
|
|
380
|
+
result.set(index, 40);
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Serialize a CellDep in Molecule format.
|
|
385
|
+
* CellDep is a struct: { out_point: OutPoint, dep_type: byte }
|
|
386
|
+
*/
|
|
387
|
+
function serializeCellDep(dep) {
|
|
388
|
+
const txHash = hexToBytes(dep.outPoint.txHash);
|
|
389
|
+
const index = writeUint32LE(parseInt(dep.outPoint.index, 16));
|
|
390
|
+
const depType = new Uint8Array([dep.depType === 'dep_group' ? 1 : 0]);
|
|
391
|
+
const result = new Uint8Array(32 + 4 + 1);
|
|
392
|
+
result.set(txHash, 0);
|
|
393
|
+
result.set(index, 32);
|
|
394
|
+
result.set(depType, 36);
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Serialize a RawTransaction in Molecule format and compute its hash.
|
|
399
|
+
*
|
|
400
|
+
* RawTransaction is a table:
|
|
401
|
+
* { version: Uint32, cell_deps: CellDepVec, header_deps: Byte32Vec,
|
|
402
|
+
* inputs: CellInputVec, outputs: CellOutputVec, outputs_data: BytesVec }
|
|
403
|
+
*/
|
|
404
|
+
function serializeRawTransaction(tx) {
|
|
405
|
+
const version = writeUint32LE(parseInt(tx.version, 16) || 0);
|
|
406
|
+
// CellDepVec: fixvec of CellDep structs (each 37 bytes)
|
|
407
|
+
const cellDeps = serializeFixvec(tx.cellDeps.map(serializeCellDep));
|
|
408
|
+
// Byte32Vec: fixvec of 32-byte hashes
|
|
409
|
+
const headerDeps = serializeFixvec(tx.headerDeps.map(h => hexToBytes(h)));
|
|
410
|
+
// CellInputVec: fixvec of CellInput structs (each 44 bytes)
|
|
411
|
+
const inputs = serializeFixvec(tx.inputs.map(serializeCellInput));
|
|
412
|
+
// CellOutputVec: dynvec of CellOutput tables
|
|
413
|
+
const outputs = serializeDynvec(tx.outputs.map(serializeCellOutput));
|
|
414
|
+
// BytesVec: dynvec of Bytes (each is 4-byte len + data)
|
|
415
|
+
const outputsData = serializeDynvec(tx.outputsData.map(d => {
|
|
416
|
+
const data = hexToBytes(d);
|
|
417
|
+
const bytes = new Uint8Array(4 + data.length);
|
|
418
|
+
bytes.set(writeUint32LE(data.length), 0);
|
|
419
|
+
bytes.set(data, 4);
|
|
420
|
+
return bytes;
|
|
421
|
+
}));
|
|
422
|
+
return serializeTable([version, cellDeps, headerDeps, inputs, outputs, outputsData]);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Compute the transaction hash (Blake2b-256 of the serialized RawTransaction).
|
|
426
|
+
*/
|
|
427
|
+
function computeTxHash(rawTx) {
|
|
428
|
+
const serialized = serializeRawTransaction(rawTx);
|
|
429
|
+
const hash = ckbBlake2b(serialized);
|
|
430
|
+
return bytesToHex(hash);
|
|
431
|
+
}
|
|
432
|
+
// ── WitnessArgs Serialization ──
|
|
433
|
+
/**
|
|
434
|
+
* Serialize WitnessArgs in Molecule format.
|
|
435
|
+
*
|
|
436
|
+
* WitnessArgs is a table: { lock: BytesOpt, input_type: BytesOpt, output_type: BytesOpt }
|
|
437
|
+
* BytesOpt: empty (0 bytes) if None, or Bytes (4-byte LE len + data)
|
|
438
|
+
*/
|
|
439
|
+
function serializeWitnessArgs(lock, inputType, outputType) {
|
|
440
|
+
const fields = [];
|
|
441
|
+
for (const field of [lock, inputType, outputType]) {
|
|
442
|
+
if (field === null) {
|
|
443
|
+
fields.push(new Uint8Array(0));
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
const bytes = new Uint8Array(4 + field.length);
|
|
447
|
+
bytes.set(writeUint32LE(field.length), 0);
|
|
448
|
+
bytes.set(field, 4);
|
|
449
|
+
fields.push(bytes);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return serializeTable(fields);
|
|
453
|
+
}
|
|
454
|
+
// ── JSON-RPC Helper ──
|
|
455
|
+
async function rpcCall(url, method, params) {
|
|
456
|
+
const response = await fetch(url, {
|
|
457
|
+
method: 'POST',
|
|
458
|
+
headers: { 'Content-Type': 'application/json' },
|
|
459
|
+
body: JSON.stringify({
|
|
460
|
+
id: 1,
|
|
461
|
+
jsonrpc: '2.0',
|
|
462
|
+
method,
|
|
463
|
+
params,
|
|
464
|
+
}),
|
|
465
|
+
});
|
|
466
|
+
if (!response.ok) {
|
|
467
|
+
throw new Error(`CKB RPC HTTP ${response.status}: ${await response.text()}`);
|
|
468
|
+
}
|
|
469
|
+
const json = await response.json();
|
|
470
|
+
if (json.error) {
|
|
471
|
+
throw new Error(`CKB RPC error: ${json.error.message} (code ${json.error.code})`);
|
|
472
|
+
}
|
|
473
|
+
return json.result;
|
|
474
|
+
}
|
|
475
|
+
// ── Adapter ──
|
|
476
|
+
class NervosAdapter {
|
|
477
|
+
/**
|
|
478
|
+
* Create a Nervos CKB adapter.
|
|
479
|
+
*
|
|
480
|
+
* @param network - Network to connect to: 'mainnet' or 'testnet'
|
|
481
|
+
* @param rpcUrl - Custom RPC URL (overrides the default for the network)
|
|
482
|
+
*/
|
|
483
|
+
constructor(network = 'mainnet', rpcUrl) {
|
|
484
|
+
this.network = network;
|
|
485
|
+
this.rpcUrl = rpcUrl || DEFAULT_RPC_URLS[network];
|
|
486
|
+
}
|
|
487
|
+
getRpcUrl() {
|
|
488
|
+
return this.rpcUrl;
|
|
489
|
+
}
|
|
490
|
+
// ────────────────────────────────────────────────
|
|
491
|
+
// Build Transaction
|
|
492
|
+
// ────────────────────────────────────────────────
|
|
493
|
+
/**
|
|
494
|
+
* Build an unsigned CKB transaction.
|
|
495
|
+
*
|
|
496
|
+
* Fetches live cells for the sender, selects inputs to cover the
|
|
497
|
+
* transfer amount plus minimum change cell capacity, constructs
|
|
498
|
+
* outputs (recipient cell + change cell), and includes the
|
|
499
|
+
* secp256k1_blake160 cell dep.
|
|
500
|
+
*
|
|
501
|
+
* The witness placeholder reserves 65 zero bytes in the lock field
|
|
502
|
+
* for the eventual secp256k1 ECDSA signature.
|
|
503
|
+
*
|
|
504
|
+
* @param tx - Transaction parameters (to address, amount in shannons)
|
|
505
|
+
* @param fromAddress - Sender CKB address (ckb1... or ckt1...)
|
|
506
|
+
* @returns Hex-encoded JSON payload with raw transaction and metadata
|
|
507
|
+
*/
|
|
508
|
+
async buildTransaction(tx, fromAddress) {
|
|
509
|
+
try {
|
|
510
|
+
const recipientLock = decodeCkbAddress(tx.to);
|
|
511
|
+
const senderLock = decodeCkbAddress(fromAddress);
|
|
512
|
+
const amount = BigInt(tx.amount);
|
|
513
|
+
if (amount < MIN_CELL_CAPACITY) {
|
|
514
|
+
throw new Error(`Amount ${amount} shannons is below minimum cell capacity ` +
|
|
515
|
+
`(${MIN_CELL_CAPACITY} shannons / ${Number(MIN_CELL_CAPACITY) / 1e8} CKB)`);
|
|
516
|
+
}
|
|
517
|
+
// Fetch live cells for the sender
|
|
518
|
+
const liveCells = await this.fetchLiveCells(senderLock);
|
|
519
|
+
if (liveCells.length === 0) {
|
|
520
|
+
throw new Error(`No live cells found for ${fromAddress}`);
|
|
521
|
+
}
|
|
522
|
+
// Select cells to cover amount + minimum change capacity
|
|
523
|
+
// We need: amount for recipient + MIN_CELL_CAPACITY for change cell
|
|
524
|
+
const needed = amount + MIN_CELL_CAPACITY;
|
|
525
|
+
const selectedCells = [];
|
|
526
|
+
let inputCapacity = 0n;
|
|
527
|
+
// Sort by capacity descending for efficient selection
|
|
528
|
+
const sorted = [...liveCells].sort((a, b) => {
|
|
529
|
+
const capA = BigInt(a.output.capacity.startsWith('0x')
|
|
530
|
+
? parseInt(a.output.capacity, 16)
|
|
531
|
+
: a.output.capacity);
|
|
532
|
+
const capB = BigInt(b.output.capacity.startsWith('0x')
|
|
533
|
+
? parseInt(b.output.capacity, 16)
|
|
534
|
+
: b.output.capacity);
|
|
535
|
+
return Number(capB - capA);
|
|
536
|
+
});
|
|
537
|
+
for (const cell of sorted) {
|
|
538
|
+
selectedCells.push(cell);
|
|
539
|
+
const cap = BigInt(cell.output.capacity.startsWith('0x')
|
|
540
|
+
? parseInt(cell.output.capacity, 16)
|
|
541
|
+
: cell.output.capacity);
|
|
542
|
+
inputCapacity += cap;
|
|
543
|
+
if (inputCapacity >= needed)
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
if (inputCapacity < needed) {
|
|
547
|
+
throw new Error(`Insufficient capacity: have ${inputCapacity} shannons, ` +
|
|
548
|
+
`need ${needed} shannons (${amount} amount + ${MIN_CELL_CAPACITY} min change)`);
|
|
549
|
+
}
|
|
550
|
+
const changeCapacity = inputCapacity - amount;
|
|
551
|
+
// Build the raw transaction
|
|
552
|
+
const cellDep = SECP256K1_BLAKE160_CELL_DEPS[this.network];
|
|
553
|
+
if (!cellDep) {
|
|
554
|
+
throw new Error(`No secp256k1_blake160 cell dep for network: ${this.network}`);
|
|
555
|
+
}
|
|
556
|
+
const inputs = selectedCells.map(cell => ({
|
|
557
|
+
previousOutput: cell.outPoint,
|
|
558
|
+
since: '0x0',
|
|
559
|
+
}));
|
|
560
|
+
const outputs = [
|
|
561
|
+
// Recipient output
|
|
562
|
+
{
|
|
563
|
+
capacity: '0x' + amount.toString(16),
|
|
564
|
+
lock: recipientLock,
|
|
565
|
+
},
|
|
566
|
+
// Change output
|
|
567
|
+
{
|
|
568
|
+
capacity: '0x' + changeCapacity.toString(16),
|
|
569
|
+
lock: senderLock,
|
|
570
|
+
},
|
|
571
|
+
];
|
|
572
|
+
const outputsData = ['0x', '0x']; // No data for simple transfers
|
|
573
|
+
const rawTransaction = {
|
|
574
|
+
version: '0x0',
|
|
575
|
+
cellDeps: [cellDep],
|
|
576
|
+
headerDeps: [],
|
|
577
|
+
inputs,
|
|
578
|
+
outputs,
|
|
579
|
+
outputsData,
|
|
580
|
+
};
|
|
581
|
+
// Create witness placeholder with 65 zero bytes for lock
|
|
582
|
+
// This is needed for signing payload computation
|
|
583
|
+
const witnessPlaceholder = serializeWitnessArgs(new Uint8Array(65), // 65 zero bytes for secp256k1 signature placeholder
|
|
584
|
+
null, null);
|
|
585
|
+
// Empty witnesses for additional inputs
|
|
586
|
+
const witnesses = [bytesToHex(witnessPlaceholder)];
|
|
587
|
+
for (let i = 1; i < inputs.length; i++) {
|
|
588
|
+
witnesses.push('0x');
|
|
589
|
+
}
|
|
590
|
+
const payload = {
|
|
591
|
+
rawTransaction,
|
|
592
|
+
witnesses,
|
|
593
|
+
txHash: computeTxHash(rawTransaction),
|
|
594
|
+
inputCapacity: inputCapacity.toString(),
|
|
595
|
+
network: this.network,
|
|
596
|
+
};
|
|
597
|
+
return Buffer.from(JSON.stringify(payload)).toString('hex');
|
|
598
|
+
}
|
|
599
|
+
catch (e) {
|
|
600
|
+
if (e instanceof errors_1.ChainError)
|
|
601
|
+
throw e;
|
|
602
|
+
throw new errors_1.ChainError(`Failed to build CKB transaction: ${e.message}`, 'nervos');
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// ────────────────────────────────────────────────
|
|
606
|
+
// Signing Payload
|
|
607
|
+
// ────────────────────────────────────────────────
|
|
608
|
+
/**
|
|
609
|
+
* Extract the signing payload from the buildTransaction output.
|
|
610
|
+
*
|
|
611
|
+
* CKB signing payload is computed as:
|
|
612
|
+
* Blake2b-256(tx_hash || witness_length || first_witness || other_witnesses...)
|
|
613
|
+
*
|
|
614
|
+
* Where tx_hash is Blake2b-256 of the serialized RawTransaction (Molecule format),
|
|
615
|
+
* witness_length is the 8-byte LE length of the first witness with placeholder,
|
|
616
|
+
* and the first witness has a 65-byte zero lock field as placeholder.
|
|
617
|
+
*
|
|
618
|
+
* @param unsignedTx - Hex-encoded unsigned transaction from buildTransaction
|
|
619
|
+
* @returns 32-byte signing hash (hex-encoded, without 0x prefix)
|
|
620
|
+
*/
|
|
621
|
+
getSigningPayload(unsignedTx) {
|
|
622
|
+
try {
|
|
623
|
+
const payload = JSON.parse(Buffer.from(unsignedTx, 'hex').toString());
|
|
624
|
+
const txHash = hexToBytes(payload.txHash);
|
|
625
|
+
// The first witness is the WitnessArgs with 65-byte zero lock placeholder
|
|
626
|
+
const firstWitness = hexToBytes(payload.witnesses[0]);
|
|
627
|
+
const firstWitnessLen = writeUint64LE(BigInt(firstWitness.length));
|
|
628
|
+
// Build the message to hash:
|
|
629
|
+
// tx_hash (32 bytes) + first_witness_length (8 bytes LE) + first_witness + other_witnesses
|
|
630
|
+
const chunks = [txHash, firstWitnessLen, firstWitness];
|
|
631
|
+
// Include remaining witnesses
|
|
632
|
+
for (let i = 1; i < payload.witnesses.length; i++) {
|
|
633
|
+
const witness = hexToBytes(payload.witnesses[i]);
|
|
634
|
+
const witnessLen = writeUint64LE(BigInt(witness.length));
|
|
635
|
+
chunks.push(witnessLen);
|
|
636
|
+
chunks.push(witness);
|
|
637
|
+
}
|
|
638
|
+
const signingHash = ckbBlake2bMulti(...chunks);
|
|
639
|
+
return Array.from(signingHash).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
return unsignedTx;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// ────────────────────────────────────────────────
|
|
646
|
+
// Attach Signature
|
|
647
|
+
// ────────────────────────────────────────────────
|
|
648
|
+
/**
|
|
649
|
+
* Attach a secp256k1 ECDSA signature to the CKB transaction.
|
|
650
|
+
*
|
|
651
|
+
* CKB expects a 65-byte recoverable signature (r: 32 bytes + s: 32 bytes + recovery_id: 1 byte)
|
|
652
|
+
* placed in the lock field of the first WitnessArgs.
|
|
653
|
+
*
|
|
654
|
+
* @param unsignedTx - Hex-encoded unsigned transaction from buildTransaction
|
|
655
|
+
* @param signature - 65-byte secp256k1 signature (hex-encoded, r + s + recovery_id)
|
|
656
|
+
* @returns Hex-encoded signed transaction payload
|
|
657
|
+
*/
|
|
658
|
+
async attachSignature(unsignedTx, signature) {
|
|
659
|
+
try {
|
|
660
|
+
const payload = JSON.parse(Buffer.from(unsignedTx, 'hex').toString());
|
|
661
|
+
const sig = signature.startsWith('0x') ? signature.slice(2) : signature;
|
|
662
|
+
if (sig.length !== 130) {
|
|
663
|
+
throw new Error(`Invalid signature length: expected 130 hex chars (65 bytes), got ${sig.length}`);
|
|
664
|
+
}
|
|
665
|
+
// Build WitnessArgs with the actual signature in the lock field
|
|
666
|
+
const signatureBytes = hexToBytes(sig);
|
|
667
|
+
const witnessArgs = serializeWitnessArgs(signatureBytes, null, null);
|
|
668
|
+
// Replace the first witness with the signed WitnessArgs
|
|
669
|
+
const witnesses = [...payload.witnesses];
|
|
670
|
+
witnesses[0] = bytesToHex(witnessArgs);
|
|
671
|
+
const signedPayload = {
|
|
672
|
+
...payload,
|
|
673
|
+
witnesses,
|
|
674
|
+
signed: true,
|
|
675
|
+
};
|
|
676
|
+
return Buffer.from(JSON.stringify(signedPayload)).toString('hex');
|
|
677
|
+
}
|
|
678
|
+
catch (e) {
|
|
679
|
+
if (e instanceof errors_1.ChainError)
|
|
680
|
+
throw e;
|
|
681
|
+
throw new errors_1.ChainError(`Failed to attach CKB signature: ${e.message}`, 'nervos');
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// ────────────────────────────────────────────────
|
|
685
|
+
// Broadcast
|
|
686
|
+
// ────────────────────────────────────────────────
|
|
687
|
+
/**
|
|
688
|
+
* Broadcast a signed CKB transaction.
|
|
689
|
+
*
|
|
690
|
+
* Calls the `send_transaction` JSON-RPC method on the CKB node.
|
|
691
|
+
*
|
|
692
|
+
* @param signedTx - Hex-encoded signed transaction from attachSignature
|
|
693
|
+
* @returns Transaction hash (0x-prefixed)
|
|
694
|
+
*/
|
|
695
|
+
async broadcast(signedTx) {
|
|
696
|
+
try {
|
|
697
|
+
const payload = JSON.parse(Buffer.from(signedTx, 'hex').toString());
|
|
698
|
+
if (!payload.signed) {
|
|
699
|
+
throw new Error('Transaction is not signed');
|
|
700
|
+
}
|
|
701
|
+
const rawTx = payload.rawTransaction;
|
|
702
|
+
// Build the full transaction object for the RPC
|
|
703
|
+
const rpcTransaction = {
|
|
704
|
+
version: rawTx.version,
|
|
705
|
+
cell_deps: rawTx.cellDeps.map((dep) => ({
|
|
706
|
+
out_point: {
|
|
707
|
+
tx_hash: dep.outPoint.txHash,
|
|
708
|
+
index: dep.outPoint.index,
|
|
709
|
+
},
|
|
710
|
+
dep_type: dep.depType === 'dep_group' ? 'dep_group' : 'code',
|
|
711
|
+
})),
|
|
712
|
+
header_deps: rawTx.headerDeps,
|
|
713
|
+
inputs: rawTx.inputs.map((input) => ({
|
|
714
|
+
previous_output: {
|
|
715
|
+
tx_hash: input.previousOutput.txHash,
|
|
716
|
+
index: input.previousOutput.index,
|
|
717
|
+
},
|
|
718
|
+
since: input.since,
|
|
719
|
+
})),
|
|
720
|
+
outputs: rawTx.outputs.map((output) => ({
|
|
721
|
+
capacity: output.capacity,
|
|
722
|
+
lock: {
|
|
723
|
+
code_hash: output.lock.codeHash,
|
|
724
|
+
hash_type: output.lock.hashType,
|
|
725
|
+
args: output.lock.args,
|
|
726
|
+
},
|
|
727
|
+
type: output.type ? {
|
|
728
|
+
code_hash: output.type.codeHash,
|
|
729
|
+
hash_type: output.type.hashType,
|
|
730
|
+
args: output.type.args,
|
|
731
|
+
} : null,
|
|
732
|
+
})),
|
|
733
|
+
outputs_data: rawTx.outputsData,
|
|
734
|
+
witnesses: payload.witnesses,
|
|
735
|
+
};
|
|
736
|
+
const txHash = await rpcCall(this.rpcUrl, 'send_transaction', [rpcTransaction, 'passthrough']);
|
|
737
|
+
return txHash;
|
|
738
|
+
}
|
|
739
|
+
catch (e) {
|
|
740
|
+
if (e instanceof errors_1.ChainError)
|
|
741
|
+
throw e;
|
|
742
|
+
throw new errors_1.ChainError(`Failed to broadcast CKB tx: ${e.message}`, 'nervos');
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// ────────────────────────────────────────────────
|
|
746
|
+
// Balance
|
|
747
|
+
// ────────────────────────────────────────────────
|
|
748
|
+
/**
|
|
749
|
+
* Get the CKB balance for an address.
|
|
750
|
+
*
|
|
751
|
+
* Returns balance in shannons (1 CKB = 1e8 shannons).
|
|
752
|
+
* Uses the `get_cells_capacity` indexer RPC method to sum
|
|
753
|
+
* all live cell capacities for the address lock script.
|
|
754
|
+
*
|
|
755
|
+
* @param address - CKB address (ckb1... or ckt1...)
|
|
756
|
+
* @returns Balance in shannons as a string
|
|
757
|
+
*/
|
|
758
|
+
async getBalance(address) {
|
|
759
|
+
try {
|
|
760
|
+
const lock = decodeCkbAddress(address);
|
|
761
|
+
const result = await rpcCall(this.rpcUrl, 'get_cells_capacity', [{
|
|
762
|
+
script: {
|
|
763
|
+
code_hash: lock.codeHash,
|
|
764
|
+
hash_type: lock.hashType,
|
|
765
|
+
args: lock.args,
|
|
766
|
+
},
|
|
767
|
+
script_type: 'lock',
|
|
768
|
+
}]);
|
|
769
|
+
if (!result || !result.capacity) {
|
|
770
|
+
return '0';
|
|
771
|
+
}
|
|
772
|
+
// capacity is returned as a hex string (e.g. "0x174876e800")
|
|
773
|
+
const capacityBigInt = BigInt(result.capacity);
|
|
774
|
+
return capacityBigInt.toString();
|
|
775
|
+
}
|
|
776
|
+
catch (e) {
|
|
777
|
+
if (e instanceof errors_1.ChainError)
|
|
778
|
+
throw e;
|
|
779
|
+
return '0';
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// ────────────────────────────────────────────────
|
|
783
|
+
// Utility Methods
|
|
784
|
+
// ────────────────────────────────────────────────
|
|
785
|
+
/**
|
|
786
|
+
* Fetch live cells for a given lock script using the CKB indexer RPC.
|
|
787
|
+
*
|
|
788
|
+
* Uses the `get_cells` JSON-RPC method to query the indexer for
|
|
789
|
+
* live (unspent) cells matching the given lock script.
|
|
790
|
+
*
|
|
791
|
+
* @param lock - Lock script to search for
|
|
792
|
+
* @returns Array of live cells
|
|
793
|
+
*/
|
|
794
|
+
async fetchLiveCells(lock) {
|
|
795
|
+
const cells = [];
|
|
796
|
+
let cursor;
|
|
797
|
+
const limit = '0x64'; // 100 cells per page
|
|
798
|
+
// Paginate through results
|
|
799
|
+
for (let page = 0; page < 10; page++) {
|
|
800
|
+
const params = [
|
|
801
|
+
{
|
|
802
|
+
script: {
|
|
803
|
+
code_hash: lock.codeHash,
|
|
804
|
+
hash_type: lock.hashType,
|
|
805
|
+
args: lock.args,
|
|
806
|
+
},
|
|
807
|
+
script_type: 'lock',
|
|
808
|
+
},
|
|
809
|
+
'asc',
|
|
810
|
+
limit,
|
|
811
|
+
];
|
|
812
|
+
if (cursor) {
|
|
813
|
+
params[3] = cursor;
|
|
814
|
+
}
|
|
815
|
+
const result = await rpcCall(this.rpcUrl, 'get_cells', cursor ? [...params.slice(0, 3), cursor] : params);
|
|
816
|
+
if (!result || !result.objects || result.objects.length === 0) {
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
for (const obj of result.objects) {
|
|
820
|
+
const cell = obj;
|
|
821
|
+
// Skip cells with type scripts (they may be special cells like UDT, NervosDAO, etc.)
|
|
822
|
+
if (cell.output.type)
|
|
823
|
+
continue;
|
|
824
|
+
cells.push({
|
|
825
|
+
outPoint: {
|
|
826
|
+
txHash: cell.out_point.tx_hash,
|
|
827
|
+
index: cell.out_point.index,
|
|
828
|
+
},
|
|
829
|
+
output: {
|
|
830
|
+
capacity: cell.output.capacity,
|
|
831
|
+
lock: {
|
|
832
|
+
codeHash: cell.output.lock.code_hash,
|
|
833
|
+
hashType: cell.output.lock.hash_type,
|
|
834
|
+
args: cell.output.lock.args,
|
|
835
|
+
},
|
|
836
|
+
type: null,
|
|
837
|
+
},
|
|
838
|
+
outputData: cell.output_data,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
cursor = result.last_cursor;
|
|
842
|
+
if (!cursor || result.objects.length < parseInt(limit, 16)) {
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return cells;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Derive a CKB address from a compressed secp256k1 public key.
|
|
850
|
+
*
|
|
851
|
+
* The default CKB lock script uses blake160 (first 20 bytes of Blake2b-256)
|
|
852
|
+
* of the public key as the args field.
|
|
853
|
+
*
|
|
854
|
+
* @param pubkeyHex - 33-byte compressed secp256k1 public key (hex-encoded)
|
|
855
|
+
* @returns CKB address (ckb1... for mainnet, ckt1... for testnet)
|
|
856
|
+
*/
|
|
857
|
+
deriveAddress(pubkeyHex) {
|
|
858
|
+
const clean = pubkeyHex.startsWith('0x') ? pubkeyHex.slice(2) : pubkeyHex;
|
|
859
|
+
if (clean.length !== 66) {
|
|
860
|
+
throw new errors_1.ChainError(`Invalid public key length: expected 33 bytes (compressed secp256k1), got ${clean.length / 2}`, 'nervos');
|
|
861
|
+
}
|
|
862
|
+
// Blake160 = first 20 bytes of Blake2b-256(public_key)
|
|
863
|
+
const pubkeyBytes = hexToBytes(clean);
|
|
864
|
+
const hash = ckbBlake2b(pubkeyBytes);
|
|
865
|
+
const blake160 = hash.slice(0, 20);
|
|
866
|
+
const args = '0x' + Array.from(blake160).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
867
|
+
// Encode as CKB2021 full address (bech32m)
|
|
868
|
+
const hrp = this.network === 'mainnet' ? 'ckb' : 'ckt';
|
|
869
|
+
const codeHashBytes = hexToBytes(SECP256K1_BLAKE160_CODE_HASH);
|
|
870
|
+
const hashTypeByte = 0x01; // type
|
|
871
|
+
// Payload: 0x00 (format) + code_hash (32) + hash_type (1) + args (20)
|
|
872
|
+
const payloadBytes = new Uint8Array(1 + 32 + 1 + 20);
|
|
873
|
+
payloadBytes[0] = 0x00; // Full format type
|
|
874
|
+
payloadBytes.set(codeHashBytes, 1);
|
|
875
|
+
payloadBytes[33] = hashTypeByte;
|
|
876
|
+
payloadBytes.set(blake160, 34);
|
|
877
|
+
// Convert to 5-bit groups
|
|
878
|
+
const data5bit = convertBits(Array.from(payloadBytes), 8, 5, true);
|
|
879
|
+
// Compute bech32m checksum
|
|
880
|
+
const checksumInput = bech32mHrpExpand(hrp).concat(data5bit).concat([0, 0, 0, 0, 0, 0]);
|
|
881
|
+
const mod = bech32mPolymod(checksumInput) ^ BECH32M_CONST;
|
|
882
|
+
const checksum = [];
|
|
883
|
+
for (let i = 0; i < 6; i++) {
|
|
884
|
+
checksum.push((mod >> (5 * (5 - i))) & 31);
|
|
885
|
+
}
|
|
886
|
+
let result = hrp + '1';
|
|
887
|
+
for (const d of data5bit.concat(checksum)) {
|
|
888
|
+
result += BECH32M_CHARSET[d];
|
|
889
|
+
}
|
|
890
|
+
return result;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
exports.NervosAdapter = NervosAdapter;
|
|
894
|
+
// ── Factory Functions ──
|
|
895
|
+
/**
|
|
896
|
+
* Create a Nervos CKB mainnet adapter.
|
|
897
|
+
*
|
|
898
|
+
* @param rpcUrl - Optional custom RPC URL
|
|
899
|
+
* @returns NervosAdapter configured for mainnet
|
|
900
|
+
*/
|
|
901
|
+
function createNervosAdapter(rpcUrl) {
|
|
902
|
+
return new NervosAdapter('mainnet', rpcUrl);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Create a Nervos CKB testnet adapter.
|
|
906
|
+
*
|
|
907
|
+
* @param rpcUrl - Optional custom RPC URL
|
|
908
|
+
* @returns NervosAdapter configured for testnet
|
|
909
|
+
*/
|
|
910
|
+
function createNervosTestnetAdapter(rpcUrl) {
|
|
911
|
+
return new NervosAdapter('testnet', rpcUrl);
|
|
912
|
+
}
|
|
913
|
+
//# sourceMappingURL=nervos.js.map
|