@carbonx3/cx3.js 0.1.1 → 0.3.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 +239 -51
- package/index.d.ts +108 -21
- package/index.js +14 -6
- package/package.json +13 -6
- package/src/abi.js +8 -1
- package/src/contract.js +5 -0
- package/src/crypto.js +17 -4
- package/src/digest.js +74 -38
- package/src/factory.js +105 -1
- package/src/provider.js +344 -59
- package/src/utils.js +19 -0
- package/src/wallet.js +63 -13
package/src/abi.js
CHANGED
|
@@ -99,6 +99,13 @@ function argsToParams(inputs, args) {
|
|
|
99
99
|
"BAD_ARGS"
|
|
100
100
|
);
|
|
101
101
|
}
|
|
102
|
+
// Built-in chain standards declare `inputs: ["params"]`: one named object
|
|
103
|
+
if (list.length === 1 && list[0] === "params") {
|
|
104
|
+
throw new Cx3Error(
|
|
105
|
+
"This method takes one named params object, e.g. transfer({ to, amount })",
|
|
106
|
+
"BAD_ARGS"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
102
109
|
if (args.length > list.length) {
|
|
103
110
|
throw new Cx3Error(
|
|
104
111
|
`Too many arguments: expected ${list.length}, got ${args.length}`,
|
|
@@ -108,7 +115,7 @@ function argsToParams(inputs, args) {
|
|
|
108
115
|
const params = {};
|
|
109
116
|
for (let i = 0; i < args.length; i++) {
|
|
110
117
|
const input = list[i];
|
|
111
|
-
const key = input.name || input.key || `arg${i}
|
|
118
|
+
const key = typeof input === "string" ? input : (input.name || input.key || `arg${i}`);
|
|
112
119
|
params[key] = args[i];
|
|
113
120
|
}
|
|
114
121
|
return params;
|
package/src/contract.js
CHANGED
|
@@ -64,6 +64,11 @@ class Contract {
|
|
|
64
64
|
const provider = this.provider;
|
|
65
65
|
const sender = this.signer ? this.signer.address : "";
|
|
66
66
|
const res = await provider.call(this.address, method, params, sender);
|
|
67
|
+
// the node answers a failed read with HTTP 200 + { success: false, error }
|
|
68
|
+
if (res && res.success === false) {
|
|
69
|
+
const code = String(res.error || res.message || "CALL_FAILED");
|
|
70
|
+
throw new Cx3Error(code, code, res);
|
|
71
|
+
}
|
|
67
72
|
return res.value !== undefined ? res.value : res;
|
|
68
73
|
}
|
|
69
74
|
|
package/src/crypto.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const crypto = require("crypto");
|
|
4
|
+
// @noble/secp256k1 3.x is an ES module: require() loads it on Node 20.19+ / 22.12+ (package.json "engines").
|
|
4
5
|
const secp = require("@noble/secp256k1");
|
|
5
6
|
const { strip0x, ensure0x } = require("./utils");
|
|
6
7
|
const { Cx3Error } = require("./errors");
|
|
@@ -48,10 +49,11 @@ async function signMessage(message, privateKeyInput) {
|
|
|
48
49
|
? Buffer.from(message, "utf8")
|
|
49
50
|
: Buffer.from(message);
|
|
50
51
|
const msgHash = sha256(msgBytes);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const
|
|
52
|
+
// msgHash is already the digest (prehash: false); deterministic RFC 6979, low-S — the same bytes 1.7.x produced.
|
|
53
|
+
// "recovered" = 65 bytes: recovery id, then r || s.
|
|
54
|
+
const signed = await secp.signAsync(msgHash, priv, { prehash: false, format: "recovered" });
|
|
55
|
+
const recovery = signed[0];
|
|
56
|
+
const signature = Buffer.from(signed.subarray(1)).toString("hex");
|
|
55
57
|
const publicKey = privateKeyToPublicKey(priv, true).toString("hex");
|
|
56
58
|
const publicKeyUncompressed = privateKeyToPublicKey(priv, false).toString("hex");
|
|
57
59
|
return {
|
|
@@ -67,6 +69,16 @@ async function signMessage(message, privateKeyInput) {
|
|
|
67
69
|
};
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Sign a site message the way the CX3Wallet extension and app do (cx3_sign_message):
|
|
74
|
+
* sha256(0x19 "CX3 Signed Message:\n" + UTF-8 length + message). Such a signature never verifies as
|
|
75
|
+
* a contract call, deploy, owner action or transaction (signing domains).
|
|
76
|
+
*/
|
|
77
|
+
async function signPersonalMessage(message, privateKeyInput) {
|
|
78
|
+
const { personalMessageBytes } = require("./digest");
|
|
79
|
+
return signMessage(personalMessageBytes(message), privateKeyInput);
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
function createPrivateKey() {
|
|
71
83
|
for (let i = 0; i < 64; i++) {
|
|
72
84
|
const priv = crypto.randomBytes(32);
|
|
@@ -86,5 +98,6 @@ module.exports = {
|
|
|
86
98
|
privateKeyToPublicKey,
|
|
87
99
|
privateKeyToAddress,
|
|
88
100
|
signMessage,
|
|
101
|
+
signPersonalMessage,
|
|
89
102
|
createPrivateKey
|
|
90
103
|
};
|
package/src/digest.js
CHANGED
|
@@ -1,41 +1,64 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { Cx3Error } = require("./errors");
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
+
* Signing domains, byte-identical to CX3GreenChain src/security/crypto/signingDomains.js
|
|
7
|
+
* (docs/security/signing-domains.md). Each starts with its own prefix, so a signature from one never
|
|
8
|
+
* verifies as another:
|
|
9
|
+
* contract call CX3_CALL_V1|[chainId, genesisHash, contract, method, params, from, timestamp]
|
|
10
|
+
* deploy CX3_DEPLOY_V1|[chainId, genesisHash, owner, standard, name, symbol, decimals, initialSupply, timestamp]
|
|
11
|
+
* owner action CX3_OWNER_V1|[chainId, genesisHash, action, address, owner, timestamp]
|
|
12
|
+
* site message 0x19 "CX3 Signed Message:\n" + UTF-8 length + message (hashPersonalMessage)
|
|
13
|
+
* chainId + genesisHash are the network's (provider.getChainIdentity()); addresses lower-case 0x + 40 hex.
|
|
6
14
|
*/
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
initialSupply = "0",
|
|
15
|
-
timestamp
|
|
16
|
-
}) {
|
|
17
|
-
return [
|
|
18
|
-
"deploy",
|
|
19
|
-
owner || "",
|
|
20
|
-
standard || "",
|
|
21
|
-
name || "",
|
|
22
|
-
symbol || "",
|
|
23
|
-
decimals !== undefined ? decimals : 18,
|
|
24
|
-
initialSupply !== undefined ? String(initialSupply) : "0",
|
|
25
|
-
timestamp || 0
|
|
26
|
-
].join("|");
|
|
16
|
+
/** lower-case 0x + 40 hex ("cx3:" / "0x" accepted), or "" */
|
|
17
|
+
function normalizeDigestAddress(addr) {
|
|
18
|
+
let s = String(addr == null ? "" : addr).trim().toLowerCase();
|
|
19
|
+
if (s.startsWith("cx3:")) s = s.slice(4);
|
|
20
|
+
if (s.startsWith("0x")) s = s.slice(2);
|
|
21
|
+
return /^[0-9a-f]{40}$/.test(s) ? `0x${s}` : "";
|
|
27
22
|
}
|
|
28
23
|
|
|
29
|
-
function
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
function requireIdentity(identity) {
|
|
25
|
+
const chainId = String((identity && identity.chainId) || "").trim();
|
|
26
|
+
const genesisHash = normalizeGenesisHash(identity && identity.genesisHash);
|
|
27
|
+
if (!chainId || !genesisHash) {
|
|
28
|
+
throw new Cx3Error("chainId and genesisHash required (provider.getChainIdentity())", "NO_CHAIN_IDENTITY");
|
|
33
29
|
}
|
|
34
|
-
return
|
|
30
|
+
return [chainId, genesisHash];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildDeployDigest({ owner, standard, name, symbol, decimals, initialSupply, timestamp }, identity) {
|
|
34
|
+
return `CX3_DEPLOY_V1|${JSON.stringify([
|
|
35
|
+
...requireIdentity(identity), normalizeDigestAddress(owner), String(standard || ""), String(name || ""),
|
|
36
|
+
String(symbol || ""), decimals == null ? 18 : Number(decimals),
|
|
37
|
+
initialSupply == null ? "0" : String(initialSupply), Number(timestamp)
|
|
38
|
+
])}`;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
function
|
|
38
|
-
return
|
|
41
|
+
function buildSendDigest(contractAddress, method, params, fromAddress, timestamp, identity) {
|
|
42
|
+
return `CX3_CALL_V1|${JSON.stringify([
|
|
43
|
+
...requireIdentity(identity), normalizeDigestAddress(contractAddress), String(method),
|
|
44
|
+
params == null ? {} : params, normalizeDigestAddress(fromAddress), Number(timestamp)
|
|
45
|
+
])}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildOwnerActionDigest(action, address, owner, timestamp, identity) {
|
|
49
|
+
return `CX3_OWNER_V1|${JSON.stringify([
|
|
50
|
+
...requireIdentity(identity), String(action), normalizeDigestAddress(address), normalizeDigestAddress(owner), Number(timestamp)
|
|
51
|
+
])}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The bytes a wallet signs for a site message: 0x19 "CX3 Signed Message:\n" + UTF-8 length + message. */
|
|
55
|
+
function personalMessageBytes(message) {
|
|
56
|
+
const msg = new TextEncoder().encode(typeof message === "string" ? message : JSON.stringify(message));
|
|
57
|
+
const prefix = new TextEncoder().encode(`\x19CX3 Signed Message:\n${msg.length}`);
|
|
58
|
+
const out = new Uint8Array(prefix.length + msg.length);
|
|
59
|
+
out.set(prefix, 0);
|
|
60
|
+
out.set(msg, prefix.length);
|
|
61
|
+
return out;
|
|
39
62
|
}
|
|
40
63
|
|
|
41
64
|
function stripNativeAddress(addr) {
|
|
@@ -46,12 +69,27 @@ function stripNativeAddress(addr) {
|
|
|
46
69
|
return s.toLowerCase();
|
|
47
70
|
}
|
|
48
71
|
|
|
72
|
+
/** "0x" + 64 lower-case hex, or null. */
|
|
73
|
+
function normalizeGenesisHash(value) {
|
|
74
|
+
const s = String(value == null ? "" : value).trim().toLowerCase();
|
|
75
|
+
const hex = s.startsWith("0x") ? s.slice(2) : s;
|
|
76
|
+
return /^[0-9a-f]{64}$/.test(hex) ? `0x${hex}` : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
/**
|
|
50
80
|
* Canonical native TRANSFER payload — byte-identical to
|
|
51
81
|
* CX3GreenChain Transaction.canonicalPayload / CX3Wallet CX3Tx.buildTransferPayload.
|
|
82
|
+
* Ends with the network's chainId and genesis hash (provider.getChainIdentity()): the node verifies
|
|
83
|
+
* against its own genesis, so a transfer signed for another network or an earlier genesis is refused.
|
|
84
|
+
* Both are required — there is no fallback a signature could silently be made under.
|
|
52
85
|
*/
|
|
53
86
|
function buildTransferPayload(fields = {}) {
|
|
54
|
-
const
|
|
87
|
+
const chainId = String(fields.chainId == null ? "" : fields.chainId).trim();
|
|
88
|
+
const genesisHash = normalizeGenesisHash(fields.genesisHash);
|
|
89
|
+
if (!chainId || !genesisHash) {
|
|
90
|
+
throw new Cx3Error("chainId and genesisHash required (provider.getChainIdentity())", "NO_CHAIN_IDENTITY");
|
|
91
|
+
}
|
|
92
|
+
return JSON.stringify({
|
|
55
93
|
type: fields.type || "TRANSFER",
|
|
56
94
|
fromAddress: stripNativeAddress(fields.fromAddress),
|
|
57
95
|
toAddress: stripNativeAddress(fields.toAddress),
|
|
@@ -60,15 +98,10 @@ function buildTransferPayload(fields = {}) {
|
|
|
60
98
|
timestamp: fields.timestamp,
|
|
61
99
|
data: fields.data || null,
|
|
62
100
|
gasLimit: typeof fields.gasLimit === "number" && fields.gasLimit > 0 ? fields.gasLimit : 0,
|
|
63
|
-
gasPrice: typeof fields.gasPrice === "number" && fields.gasPrice > 0 ? fields.gasPrice : 0
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
: process.env.CX3_CHAIN_ID_BIND !== "0";
|
|
68
|
-
if (bind) {
|
|
69
|
-
payload.chainId = String(fields.chainId || process.env.CX3_CHAIN_ID || "cx3-dev");
|
|
70
|
-
}
|
|
71
|
-
return JSON.stringify(payload);
|
|
101
|
+
gasPrice: typeof fields.gasPrice === "number" && fields.gasPrice > 0 ? fields.gasPrice : 0,
|
|
102
|
+
chainId,
|
|
103
|
+
genesisHash
|
|
104
|
+
});
|
|
72
105
|
}
|
|
73
106
|
|
|
74
107
|
module.exports = {
|
|
@@ -76,5 +109,8 @@ module.exports = {
|
|
|
76
109
|
buildSendDigest,
|
|
77
110
|
buildOwnerActionDigest,
|
|
78
111
|
buildTransferPayload,
|
|
112
|
+
personalMessageBytes,
|
|
113
|
+
normalizeDigestAddress,
|
|
114
|
+
normalizeGenesisHash,
|
|
79
115
|
stripNativeAddress
|
|
80
116
|
};
|
package/src/factory.js
CHANGED
|
@@ -2,9 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
const { Cx3Error } = require("./errors");
|
|
4
4
|
const { Contract } = require("./contract");
|
|
5
|
+
const { normalizeAddress, isPlainObject } = require("./utils");
|
|
6
|
+
|
|
7
|
+
// the token standards the CX3WebUI deploys as Public or Private
|
|
8
|
+
const PRIVATE_STANDARDS = new Set(["CX30", "CX31", "CX300", "CX315", "CX3192"]);
|
|
9
|
+
const MAX_AUTHORIZED_WALLETS = 200;
|
|
10
|
+
|
|
11
|
+
function authorizedWalletList(list) {
|
|
12
|
+
if (list == null) return [];
|
|
13
|
+
if (!Array.isArray(list)) {
|
|
14
|
+
throw new Cx3Error("authorizedWallets is an array of wallet addresses", "BAD_DEPLOY_ARGS");
|
|
15
|
+
}
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const raw of list) {
|
|
18
|
+
const a = normalizeAddress(raw);
|
|
19
|
+
if (!/^0x[0-9a-f]{40}$/.test(a)) {
|
|
20
|
+
throw new Cx3Error("authorizedWallets: every entry is a wallet address (0x + 40 hex)", "INVALID_AUTHORIZED_WALLET", { wallet: String(raw) });
|
|
21
|
+
}
|
|
22
|
+
if (!out.includes(a)) out.push(a);
|
|
23
|
+
}
|
|
24
|
+
if (out.length > MAX_AUTHORIZED_WALLETS) {
|
|
25
|
+
throw new Cx3Error(`authorizedWallets takes at most ${MAX_AUTHORIZED_WALLETS} wallets`, "TOO_MANY_AUTHORIZED_WALLETS");
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// addToWhitelist params by the contract's ABI names (addr or account, then kycHash / invType = "")
|
|
31
|
+
function whitelistParams(def, wallet) {
|
|
32
|
+
const names = (def && Array.isArray(def.inputs) ? def.inputs : [])
|
|
33
|
+
.map((i) => (typeof i === "string" ? i : i && i.name))
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
if (!names.length) return { addr: wallet, kycHash: "" };
|
|
36
|
+
const params = {};
|
|
37
|
+
names.forEach((n, i) => { params[n] = i === 0 ? wallet : ""; });
|
|
38
|
+
return params;
|
|
39
|
+
}
|
|
5
40
|
|
|
6
41
|
/**
|
|
7
42
|
* Standard token deploy helpers.
|
|
43
|
+
*
|
|
44
|
+
* `visibility: "private"` deploys a private token, like the CX3WebUI "Private" choice: only the owner
|
|
45
|
+
* and `authorizedWallets` send, receive and see it. The chain renders a private CX30 itself; CX31,
|
|
46
|
+
* CX300, CX315 and CX3192 take their CX3WebUI source (`sourceCode`). Each authorized wallet is added
|
|
47
|
+
* with one signed `addToWhitelist` after the deploy (`result.authorized`).
|
|
8
48
|
*/
|
|
9
49
|
class Cx3TokenFactory {
|
|
10
50
|
/**
|
|
@@ -104,8 +144,18 @@ class Cx3TokenFactory {
|
|
|
104
144
|
if (!fields.name || !fields.symbol) {
|
|
105
145
|
throw new Cx3Error("name and symbol are required", "BAD_DEPLOY_ARGS");
|
|
106
146
|
}
|
|
147
|
+
const { visibility, authorizedWallets, ...rest } = fields;
|
|
148
|
+
let body = rest;
|
|
149
|
+
let wallets = [];
|
|
150
|
+
if (visibility === "private") {
|
|
151
|
+
({ body, wallets } = this._privateFields(standard, rest, authorizedWallets));
|
|
152
|
+
} else if (visibility != null && visibility !== "public") {
|
|
153
|
+
throw new Cx3Error('visibility is "public" or "private"', "BAD_DEPLOY_ARGS");
|
|
154
|
+
} else if (authorizedWallets != null) {
|
|
155
|
+
throw new Cx3Error('authorizedWallets belongs to visibility: "private"', "BAD_DEPLOY_ARGS");
|
|
156
|
+
}
|
|
107
157
|
const result = await this.wallet.deployContract({
|
|
108
|
-
...
|
|
158
|
+
...body,
|
|
109
159
|
standard
|
|
110
160
|
});
|
|
111
161
|
if (result && result.success !== false && result.contractAddress) {
|
|
@@ -122,9 +172,63 @@ class Cx3TokenFactory {
|
|
|
122
172
|
this.wallet
|
|
123
173
|
);
|
|
124
174
|
}
|
|
175
|
+
if (wallets.length) result.authorized = await this._authorize(result.contract, wallets);
|
|
125
176
|
}
|
|
126
177
|
return result;
|
|
127
178
|
}
|
|
179
|
+
|
|
180
|
+
_privateFields(standard, fields, list) {
|
|
181
|
+
if (!PRIVATE_STANDARDS.has(standard)) {
|
|
182
|
+
throw new Cx3Error(`${standard} has no private variant (CX30, CX31, CX300, CX315 and CX3192 do)`, "PRIVATE_NOT_SUPPORTED");
|
|
183
|
+
}
|
|
184
|
+
const wallets = authorizedWalletList(list);
|
|
185
|
+
const hasSource = !!(fields.sourceCode || fields.bytecode);
|
|
186
|
+
if (!hasSource && (standard !== "CX30" || (fields.target && fields.target !== "CX3WASM"))) {
|
|
187
|
+
throw new Cx3Error(
|
|
188
|
+
`A private ${standard} ${standard === "CX30" ? "on CX3JS " : ""}is deployed from its CX3WebUI source (sourceCode); the chain renders only a CX3WASM CX30 itself`,
|
|
189
|
+
"PRIVATE_NEEDS_SOURCE"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const metadata = {
|
|
193
|
+
...(isPlainObject(fields.metadata) ? fields.metadata : {}),
|
|
194
|
+
visibility: "private",
|
|
195
|
+
authorizedWallets: wallets.length
|
|
196
|
+
};
|
|
197
|
+
const body = { ...fields, whitelistable: true, metadata };
|
|
198
|
+
if (!hasSource) {
|
|
199
|
+
// the chain renders CX30 from constructorArgs; whitelistable adds the authorized list
|
|
200
|
+
// (isWhitelisted, addToWhitelist / removeFromWhitelist) and the transfer guards
|
|
201
|
+
body.constructorArgs = {
|
|
202
|
+
name: fields.name,
|
|
203
|
+
symbol: fields.symbol,
|
|
204
|
+
decimals: fields.decimals !== undefined ? Number(fields.decimals) : 18,
|
|
205
|
+
totalSupply: fields.initialSupply || fields.totalSupply || "0",
|
|
206
|
+
owner: fields.owner || this.wallet.address,
|
|
207
|
+
mintable: fields.mintable !== undefined ? fields.mintable : true,
|
|
208
|
+
burnable: fields.burnable !== undefined ? fields.burnable : true,
|
|
209
|
+
pausable: fields.pausable !== undefined ? fields.pausable : true,
|
|
210
|
+
blacklistable: !!fields.blacklistable,
|
|
211
|
+
...(isPlainObject(fields.constructorArgs) ? fields.constructorArgs : {}),
|
|
212
|
+
whitelistable: true
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return { body, wallets };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// one signed addToWhitelist per wallet (the CX3WebUI writes them into the constructor instead)
|
|
219
|
+
async _authorize(contract, wallets) {
|
|
220
|
+
const def = contract.interface && contract.interface.addToWhitelist;
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const w of wallets) {
|
|
223
|
+
try {
|
|
224
|
+
const tx = await this.wallet.sendTransaction(contract.address, "addToWhitelist", whitelistParams(def, w));
|
|
225
|
+
out.push({ wallet: w, success: tx.success !== false, txHash: tx.txHash || null });
|
|
226
|
+
} catch (e) {
|
|
227
|
+
out.push({ wallet: w, success: false, error: (e && (e.code || e.message)) || "SEND_FAILED" });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
128
232
|
}
|
|
129
233
|
|
|
130
234
|
module.exports = { Cx3TokenFactory };
|