@carbonx3/cx3.js 0.3.8 → 0.3.9

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 CHANGED
@@ -219,7 +219,7 @@ A private CX31, CX300, CX315 or CX3192 takes its CX3WebUI source as `sourceCode`
219
219
 
220
220
  Digests are UTF-8 strings, hashed with SHA-256 and signed with secp256k1, byte-identical to what the Green Chain node verifies. Each kind starts with its own prefix (signing domains), so a signature of one kind never verifies as another; `chainId` and `genesisHash` are the network's (`provider.getChainIdentity()`, fetched by the wallet methods themselves) and addresses are lower-case `0x` + 40 hex:
221
221
 
222
- - **Deploy:** `CX3_DEPLOY_V1|` + `JSON.stringify([chainId, genesisHash, owner, standard, name, symbol, decimals, initialSupply, timestamp])`
222
+ - **Deploy:** `CX3_DEPLOY_V2|` + `JSON.stringify([chainId, genesisHash, owner, standard, name, symbol, decimals, initialSupply, contentHash, timestamp])`. `contentHash` is `Cx3Wallet.deployContentHash(request)`: the SHA-256 of the code, the constructor arguments, the target and the template switches of the request, so the signature is good for that content only. `deployContract` computes it, signs it and sends it. `signDeploy` without a `contentHash` signs the older `CX3_DEPLOY_V1` (the same list without it), which the network still accepts.
223
223
  - **Contract send:** `CX3_CALL_V1|` + `JSON.stringify([chainId, genesisHash, contractAddress, method, params, fromAddress, timestamp])`. The chain checks this signature again inside the relayed transaction: a call runs as your wallet only with it.
224
224
  - **Owner action:** `CX3_OWNER_V1|` + `JSON.stringify([chainId, genesisHash, action, address, owner, timestamp])`
225
225
  - **Site message:** `0x19` + `"CX3 Signed Message:\n"` + UTF-8 length + message (`signPersonalMessage`), the way the CX3Wallet extension and app sign `cx3_sign_message`
@@ -286,7 +286,7 @@ Type declarations (`index.d.ts`) ship with the package.
286
286
 
287
287
  ## Upgrading from 0.2.x
288
288
 
289
- Version 0.3.0 signs native transfers for the network's new signing format (0.3.1 to 0.3.4 change only this README, the file headers and comments, not the code). In 0.3.5, `bindApiKey` with a wallet API key asks the gateway's `/wallet/api-key/bind` (before, it asked `/whoami`, which the gateway refuses, so it always failed). In 0.3.6, `wait()` follows a send or deploy the gateway reports as not final (`dropped`) until its `finalAfter` instead of failing after 60 s, a `TX_TIMEOUT` answer with a hash is pending, `getRequest()` looks up a signed request, and `transferNative` refuses more than 6 decimals. 0.3.7 changes only this README: a native transfer's gas price floor is 0.0001 (the 0.3.6 README said 0.000001). 0.3.8 changes only this README too: the CX3 IIFE download link names `cx3-0.2.9.min.js`.
289
+ Version 0.3.0 signs native transfers for the network's new signing format (0.3.1 to 0.3.4 change only this README, the file headers and comments, not the code). In 0.3.5, `bindApiKey` with a wallet API key asks the gateway's `/wallet/api-key/bind` (before, it asked `/whoami`, which the gateway refuses, so it always failed). In 0.3.6, `wait()` follows a send or deploy the gateway reports as not final (`dropped`) until its `finalAfter` instead of failing after 60 s, a `TX_TIMEOUT` answer with a hash is pending, `getRequest()` looks up a signed request, and `transferNative` refuses more than 6 decimals. 0.3.7 changes only this README: a native transfer's gas price floor is 0.0001 (the 0.3.6 README said 0.000001). 0.3.8 changes only this README too: the CX3 IIFE download link names `cx3-0.2.9.min.js`. In 0.3.9, `deployContract` signs `CX3_DEPLOY_V2`: the code and the constructor arguments of the request are under the signature (`Cx3Wallet.deployContentHash`, `signDeploy({ contentHash })`, `deployContentText`).
290
290
 
291
291
  - **The genesis hash is signed.** `transferNative` asks the network for its chain id and genesis hash (`provider.getChainIdentity()`) unless both are given as `opts.chainId` and `opts.genesisHash`. 0.2.x transfers no longer verify on the chain.
292
292
  - **No fallback chain id.** `buildTransferPayload` throws `NO_CHAIN_IDENTITY` without `chainId` and `genesisHash`; the `CX3_CHAIN_ID` variable and the `chainIdBind` option are gone.
package/index.d.ts CHANGED
@@ -149,7 +149,10 @@ export class Cx3Wallet {
149
149
  /** A site message the way the CX3Wallet extension and app sign it (0x19 "CX3 Signed Message:\n" prefix) */
150
150
  signPersonalMessage(message: string): Promise<any>;
151
151
  /** Typed digests (CX3_DEPLOY_V1 / CX3_CALL_V1 / CX3_OWNER_V1) bound to the network's chainId + genesisHash (from the provider unless given in opts) */
152
+ /** With fields.contentHash (Cx3Wallet.deployContentHash(request)) the digest is CX3_DEPLOY_V2; the answer carries digestVersion 2 | 1 */
152
153
  signDeploy(fields: object, opts?: Cx3ChainIdentity): Promise<any>;
154
+ /** sha256 (hex) of the code, constructor arguments, target and template switches of a deploy request */
155
+ static deployContentHash(request: object): string;
153
156
  signSend(contractAddress: string, method: string, params?: object, opts?: Cx3ChainIdentity): Promise<any>;
154
157
  signOwnerAction(action: string, address: string, owner?: string | null, opts?: Cx3ChainIdentity): Promise<any>;
155
158
  /** A private token's transfer / transferFrom / mint is checked first (PRIVATE_TOKEN_SENDER / _RECIPIENT); `privateCheck: false` skips it */
@@ -263,8 +266,10 @@ export function ensure0x(hex: string): string;
263
266
  export function strip0x(hex: string): string;
264
267
  /** The network a typed digest or a native transfer is bound to (provider.getChainIdentity()) */
265
268
  export interface Cx3ChainIdentity { chainId?: string; genesisHash?: string }
266
- /** CX3_DEPLOY_V1|[chainId, genesisHash, owner, standard, name, symbol, decimals, initialSupply, timestamp] */
269
+ /** With fields.contentHash: CX3_DEPLOY_V2|[chainId, genesisHash, owner, standard, name, symbol, decimals, initialSupply, contentHash, timestamp]; without: CX3_DEPLOY_V1 (the same list without it) */
267
270
  export function buildDeployDigest(fields: object, identity: Cx3ChainIdentity): string;
271
+ /** The text whose sha256 (hex) is a deploy request's contentHash */
272
+ export function deployContentText(request: object): string;
268
273
  /** CX3_CALL_V1|[chainId, genesisHash, contract, method, params, from, timestamp] */
269
274
  export function buildSendDigest(contractAddress: string, method: string, params: object, fromAddress: string, timestamp: number, identity: Cx3ChainIdentity): string;
270
275
  /** CX3_OWNER_V1|[chainId, genesisHash, action, address, owner, timestamp] */
package/index.js CHANGED
@@ -20,6 +20,7 @@ const {
20
20
  } = require("./src/utils");
21
21
  const {
22
22
  buildDeployDigest,
23
+ deployContentText,
23
24
  buildSendDigest,
24
25
  buildOwnerActionDigest,
25
26
  buildTransferPayload,
@@ -33,7 +34,7 @@ const {
33
34
  } = require("./src/crypto");
34
35
  const { normalizeAbi, argsToParams } = require("./src/abi");
35
36
 
36
- const VERSION = "0.3.8";
37
+ const VERSION = "0.3.9";
37
38
 
38
39
  /**
39
40
  * Convenience: provider from `options.url` or env `CX3_NODE_URL` — the CX3Api gateway
@@ -65,6 +66,7 @@ module.exports = {
65
66
  walletApiKeyMode,
66
67
  toJsonSafe,
67
68
  buildDeployDigest,
69
+ deployContentText,
68
70
  buildSendDigest,
69
71
  buildOwnerActionDigest,
70
72
  buildTransferPayload,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carbonx3/cx3.js",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "Official Node.js SDK (CX3js) for CarbonX3 Green Chain: OAuth2 app access, local secp256k1 signing, ABI-bound contracts and token deploys over REST + JSON",
5
5
  "homepage": "https://cx3script.com",
6
6
  "publishConfig": {
package/src/digest.js CHANGED
@@ -34,7 +34,48 @@ function requireIdentity(identity) {
34
34
  return [chainId, genesisHash];
35
35
  }
36
36
 
37
- function buildDeployDigest({ owner, standard, name, symbol, decimals, initialSupply, timestamp }, identity) {
37
+ /** JSON with object keys in order, undefined as null (the node's signingDomains.canonicalJson). */
38
+ function canonicalJson(v) {
39
+ if (v === undefined || v === null || typeof v === "function" || typeof v === "symbol") return "null";
40
+ if (typeof v === "bigint") return JSON.stringify(v.toString());
41
+ if (typeof v !== "object") return JSON.stringify(v) === undefined ? "null" : JSON.stringify(v);
42
+ if (Array.isArray(v)) return `[${v.map(canonicalJson).join(",")}]`;
43
+ return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(v[k])}`).join(",")}}`;
44
+ }
45
+
46
+ /**
47
+ * The text whose sha256 (hex) is a deploy request's contentHash — what the request deploys beyond the
48
+ * CX3_DEPLOY_V1 fields: the code as sent, the constructor arguments, the target, the template switches.
49
+ * Byte-identical to the node's signingDomains.deployContentHash input.
50
+ */
51
+ function deployContentText(body) {
52
+ const b = body && typeof body === "object" ? body : {};
53
+ const flag = (v) => (v === undefined || v === null ? null : (v === true || v === "true"));
54
+ return canonicalJson({
55
+ sourceCode: String(b.sourceCode || b.bytecode || ""),
56
+ constructorArgs: b.constructorArgs === undefined ? null : b.constructorArgs,
57
+ target: String(b.target || ""),
58
+ mintable: flag(b.mintable),
59
+ burnable: flag(b.burnable),
60
+ pausable: flag(b.pausable),
61
+ totalSupply: b.totalSupply === undefined || b.totalSupply === null ? null : String(b.totalSupply)
62
+ });
63
+ }
64
+
65
+ /**
66
+ * With `contentHash` (64 hex, sha256 of deployContentText) the digest is CX3_DEPLOY_V2: the code and
67
+ * the constructor arguments are under the signature too. Without it, CX3_DEPLOY_V1.
68
+ */
69
+ function buildDeployDigest({ owner, standard, name, symbol, decimals, initialSupply, contentHash, timestamp }, identity) {
70
+ const hash = String(contentHash == null ? "" : contentHash).toLowerCase();
71
+ if (hash) {
72
+ if (!/^[0-9a-f]{64}$/.test(hash)) throw new Cx3Error("contentHash must be 64 hex characters", "INVALID_CONTENT_HASH");
73
+ return `CX3_DEPLOY_V2|${JSON.stringify([
74
+ ...requireIdentity(identity), normalizeDigestAddress(owner), String(standard || ""), String(name || ""),
75
+ String(symbol || ""), decimals == null ? 18 : Number(decimals),
76
+ initialSupply == null ? "0" : String(initialSupply), hash, Number(timestamp)
77
+ ])}`;
78
+ }
38
79
  return `CX3_DEPLOY_V1|${JSON.stringify([
39
80
  ...requireIdentity(identity), normalizeDigestAddress(owner), String(standard || ""), String(name || ""),
40
81
  String(symbol || ""), decimals == null ? 18 : Number(decimals),
@@ -109,6 +150,8 @@ function buildTransferPayload(fields = {}) {
109
150
  }
110
151
 
111
152
  module.exports = {
153
+ canonicalJson,
154
+ deployContentText,
112
155
  buildDeployDigest,
113
156
  buildSendDigest,
114
157
  buildOwnerActionDigest,
package/src/wallet.js CHANGED
@@ -9,11 +9,12 @@ const {
9
9
  privateKeyToPublicKey,
10
10
  signMessage,
11
11
  signPersonalMessage,
12
- createPrivateKey
12
+ createPrivateKey,
13
+ sha256
13
14
  } = require("./crypto");
14
15
  const { normalizeAddress, toJsonSafe } = require("./utils");
15
16
  const { Cx3Error } = require("./errors");
16
- const { buildDeployDigest, buildSendDigest, buildOwnerActionDigest, buildTransferPayload, stripNativeAddress } = require("./digest");
17
+ const { buildDeployDigest, buildSendDigest, buildOwnerActionDigest, buildTransferPayload, stripNativeAddress, deployContentText } = require("./digest");
17
18
  const { TransactionResponse } = require("./transaction");
18
19
 
19
20
  /** Green Chain deploy rule: a token symbol is capital letters A-Z and digits 0-9 only. */
@@ -101,9 +102,15 @@ class Cx3Wallet {
101
102
  return this.provider.getChainIdentity();
102
103
  }
103
104
 
105
+ /**
106
+ * Sign a deploy request. With `fields.contentHash` (Cx3Wallet.deployContentHash(request)) the digest is
107
+ * CX3_DEPLOY_V2: the code and the constructor arguments are under the signature, and the request
108
+ * must carry the same `contentHash`. Without it, CX3_DEPLOY_V1.
109
+ */
104
110
  async signDeploy(fields, opts = {}) {
105
111
  const owner = normalizeAddress(fields.owner || this.address);
106
112
  const timestamp = fields.timestamp || Date.now();
113
+ const contentHash = fields.contentHash ? String(fields.contentHash).toLowerCase() : undefined;
107
114
  const digest = buildDeployDigest({
108
115
  owner,
109
116
  standard: fields.standard,
@@ -111,6 +118,7 @@ class Cx3Wallet {
111
118
  symbol: fields.symbol,
112
119
  decimals: fields.decimals,
113
120
  initialSupply: fields.initialSupply,
121
+ contentHash,
114
122
  timestamp
115
123
  }, await this._identity(opts));
116
124
  const signed = await this.signMessage(digest);
@@ -118,10 +126,18 @@ class Cx3Wallet {
118
126
  ...signed,
119
127
  owner,
120
128
  timestamp,
121
- digest
129
+ digest,
130
+ digestVersion: contentHash ? 2 : 1,
131
+ ...(contentHash ? { contentHash } : {})
122
132
  };
123
133
  }
124
134
 
135
+ /** sha256 (hex) of what a deploy request deploys beyond the named fields — the node's deployContentHash. */
136
+ static deployContentHash(request) {
137
+ const asSent = JSON.parse(JSON.stringify(toJsonSafe(request || {})));
138
+ return Buffer.from(sha256(deployContentText(asSent))).toString("hex");
139
+ }
140
+
125
141
  async signSend(contractAddress, method, params, opts = {}) {
126
142
  const fromAddress = this.address;
127
143
  const timestamp = Date.now();
@@ -196,9 +212,12 @@ class Cx3Wallet {
196
212
  );
197
213
  }
198
214
  }
215
+ // CX3_DEPLOY_V2: the code and the constructor arguments of this request are signed too
216
+ const contentHash = Cx3Wallet.deployContentHash(fields);
199
217
  const signed = await this.signDeploy({
200
218
  ...fields,
201
- owner: fields.owner || this.address
219
+ owner: fields.owner || this.address,
220
+ contentHash
202
221
  });
203
222
  const body = {
204
223
  ...fields,
@@ -207,7 +226,8 @@ class Cx3Wallet {
207
226
  wallet: fields.wallet || signed.owner,
208
227
  timestamp: signed.timestamp,
209
228
  signature: signed.signature,
210
- senderPublicKey: signed.senderPublicKey
229
+ senderPublicKey: signed.senderPublicKey,
230
+ contentHash
211
231
  };
212
232
  const result = await this.provider.request("/deployContract", {
213
233
  method: "POST",