@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/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
- const signed = await secp.sign(msgHash, priv, { der: false, recovered: true });
52
- const sigBytes = Array.isArray(signed) ? signed[0] : signed;
53
- const recovery = Array.isArray(signed) ? signed[1] : (signed.recovery != null ? signed.recovery : 0);
54
- const signature = Buffer.from(sigBytes).toString("hex");
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
- * Canonical digests must stay byte-identical to
5
- * CX3GreenChain/src/api/controllers/ContractController.js
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
- function buildDeployDigest({
9
- owner,
10
- standard,
11
- name,
12
- symbol,
13
- decimals = 18,
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 buildSendDigest(contractAddress, method, params, fromAddress, timestamp) {
30
- const base = `${contractAddress}|${method}|${JSON.stringify(params || {})}|${fromAddress || ""}`;
31
- if (timestamp !== undefined && timestamp !== null && timestamp !== "") {
32
- return `${base}|${timestamp}`;
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 base;
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 buildOwnerActionDigest(action, address, owner, timestamp) {
38
- return `${action}|${address}|${owner}|${timestamp}`;
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 payload = {
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
- const bind = fields.chainIdBind != null
66
- ? !!fields.chainIdBind
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
- ...fields,
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 };