@carbonx3/cx3.js 0.2.0 → 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 CHANGED
@@ -8,7 +8,7 @@ The official Node.js SDK (**CX3js**) for [CarbonX3 Green Chain](https://carbonx3
8
8
 
9
9
  CX3js connects server-side applications to CarbonX3 Green Chain through the CX3Api gateway. It signs in as an OAuth2 application, signs transactions locally with secp256k1, binds contract ABIs to plain JavaScript methods and deploys the Green Chain token standards. Requests are REST + JSON with named parameters, not binary calldata.
10
10
 
11
- > **Network status:** CarbonX3 Green Chain currently runs as a development network (devnet, chain id `cx3-dev`). Devnet CX3, tokens and contracts are test data with no monetary value and may be cleared when the devnet is reset.
11
+ > **Network status:** CarbonX3 Green Chain currently runs as a development network (devnet, chain id `cx3-dev-2`). Devnet CX3, tokens and contracts are test data with no monetary value and may be cleared when the devnet is reset.
12
12
 
13
13
  ## Features
14
14
 
@@ -105,7 +105,7 @@ const provider = new Cx3Provider("https://cx3api.carbonx3.com", { apiKey: proces
105
105
  Public chain data, served by the gateway to every application (a development node answers `DAPP_ONLY`):
106
106
 
107
107
  ```javascript
108
- const status = await provider.getStatus(); // { height, hash, timestamp, chainId }
108
+ const status = await provider.getStatus(); // { height, hash, timestamp, chainId, genesisHash }
109
109
  const { tokens } = await provider.getTokens(wallet.address); // every token with this wallet's balance
110
110
  const page = await provider.getActivity(wallet.address, { limit: 20 }); // newest first, no block rewards
111
111
  const older = await provider.getActivity(wallet.address, { before: page.nextBefore });
@@ -199,16 +199,17 @@ A private CX31, CX300, CX315 or CX3192 takes its CX3WebUI source as `sourceCode`
199
199
 
200
200
  ## Signing
201
201
 
202
- Digests are UTF-8 strings, hashed with SHA-256 and signed with secp256k1, byte-identical to what the Green Chain node verifies:
202
+ 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:
203
203
 
204
- - **Deploy:** `deploy|{owner}|{standard}|{name}|{symbol}|{decimals}|{initialSupply}|{timestamp}`
205
- - **Contract send:** `{contractAddress}|{method}|{JSON.stringify(params)}|{fromAddress}|{timestamp}`
206
- - **Owner action:** `{action}|{address}|{owner}|{timestamp}`
207
- - **Native transfer:** `JSON.stringify({ type, fromAddress, toAddress, amount, nonce, timestamp, data, gasLimit, gasPrice, chainId })`, with 40-hex addresses and no `0x`
204
+ - **Deploy:** `CX3_DEPLOY_V1|` + `JSON.stringify([chainId, genesisHash, owner, standard, name, symbol, decimals, initialSupply, timestamp])`
205
+ - **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.
206
+ - **Owner action:** `CX3_OWNER_V1|` + `JSON.stringify([chainId, genesisHash, action, address, owner, timestamp])`
207
+ - **Site message:** `0x19` + `"CX3 Signed Message:\n"` + UTF-8 length + message (`signPersonalMessage`), the way the CX3Wallet extension and app sign `cx3_sign_message`
208
+ - **Native transfer:** `JSON.stringify({ type, fromAddress, toAddress, amount, nonce, timestamp, data, gasLimit, gasPrice, chainId, genesisHash })`, with 40-hex addresses and no `0x`; `chainId` and `genesisHash` (`0x` + 64 hex) are the network's, from `provider.getChainIdentity()`
208
209
 
209
- BigInt values in params are converted to decimal strings before signing, and exactly those params are sent. Signatures carry a timestamp and are accepted within ±5 minutes of the node's clock. An address is `0x` + the last 20 bytes of `sha256(uncompressedPublicKey)`.
210
+ BigInt values in params are converted to decimal strings before signing, and exactly those params are sent. Signatures carry a timestamp and are accepted within ±5 minutes of the node's clock; a native transfer is taken only by a block within 10 minutes of its timestamp. The genesis hash ties a native transfer to one network and one genesis: after a chain reset, a transfer signed before it no longer verifies. An address is `0x` + the last 20 bytes of `sha256(uncompressedPublicKey)`.
210
211
 
211
- The same building blocks are exported: `buildDeployDigest`, `buildSendDigest`, `buildOwnerActionDigest`, `buildTransferPayload`, `signMessage`, `privateKeyToAddress` and `createPrivateKey`.
212
+ The same building blocks are exported: `buildDeployDigest`, `buildSendDigest`, `buildOwnerActionDigest` (each takes the identity as its last argument), `buildTransferPayload`, `personalMessageBytes`, `signMessage` (signs a digest as given), `signPersonalMessage`, `privateKeyToAddress` and `createPrivateKey`.
212
213
 
213
214
  ## Error handling
214
215
 
@@ -237,6 +238,7 @@ try {
237
238
  | `PRIVATE_TOKEN_SENDER`, `PRIVATE_TOKEN_RECIPIENT`, `PRIVATE_TOKEN` | Private token rules (see above) |
238
239
  | `TX_FAILED`, `TX_TRACK_TIMEOUT` | The transaction failed, or `tx.wait()` did not see it confirmed in time |
239
240
  | `BASIC_AUTH_UNSUPPORTED` | The `auth` option was used |
241
+ | `NO_CHAIN_IDENTITY` | The network's chain id or genesis hash is unavailable; a native transfer is not signed without them |
240
242
 
241
243
  ## Browser applications
242
244
 
@@ -249,12 +251,20 @@ This package is for Node.js servers. Web pages use the **CX3 IIFE** (`cx3.min.js
249
251
  | `CX3_CLIENT_ID`, `CX3_CLIENT_SECRET` | Application credentials when the options are omitted |
250
252
  | `CX3_API_KEY` | Legacy wallet API key |
251
253
  | `CX3_NODE_URL` | Gateway URL for `getDefaultProvider()` |
252
- | `CX3_CHAIN_ID` | Chain id in native transfer signatures (default `cx3-dev`) |
253
254
 
254
255
  ## TypeScript
255
256
 
256
257
  Type declarations (`index.d.ts`) ship with the package.
257
258
 
259
+ ## Upgrading from 0.2.x
260
+
261
+ Version 0.3.0 signs native transfers for the network's new signing format:
262
+
263
+ - **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.
264
+ - **No fallback chain id.** `buildTransferPayload` throws `NO_CHAIN_IDENTITY` without `chainId` and `genesisHash`; the `CX3_CHAIN_ID` variable and the `chainIdBind` option are gone.
265
+ - **Typed digests.** Contract sends, deploys and owner actions sign `CX3_CALL_V1` / `CX3_DEPLOY_V1` / `CX3_OWNER_V1` digests bound to the network (see Signing); the `|`-joined digests of 0.2.x are refused. `signSend`, `signDeploy` and `signOwnerAction` take an optional `{ chainId, genesisHash }`, else they ask the provider (kept for a minute). The `build*Digest` helpers need the identity as their last argument.
266
+ - **Site messages.** `signPersonalMessage` signs the way the wallets sign `cx3_sign_message` (with the `0x19` prefix); `signMessage` still signs a digest as it is.
267
+
258
268
  ## Upgrading from 0.1.x
259
269
 
260
270
  CX3js is in beta (0.x), so a minor version may contain breaking changes. Version 0.2.0:
package/index.d.ts CHANGED
@@ -81,8 +81,10 @@ export class Cx3Provider {
81
81
  getAbi(address: string): Promise<any>;
82
82
  /** WIT interface of a CX3WASM contract */
83
83
  getWit(address: string): Promise<{ success: boolean; address: string; target: string | null; wit: string }>;
84
- /** Chain head, with chainId from the gateway */
85
- getStatus(): Promise<{ success: boolean; height: number; hash: string | null; timestamp: number | null; chainId?: string | null }>;
84
+ /** Chain head, with chainId and genesisHash from the gateway */
85
+ getStatus(): Promise<{ success: boolean; height: number; hash: string | null; timestamp: number | null; chainId?: string | null; genesisHash?: string | null }>;
86
+ /** The network identity a native transfer signs over; throws NO_CHAIN_IDENTITY when unavailable */
87
+ getChainIdentity(): Promise<{ chainId: string; genesisHash: string }>;
86
88
  getContractInfo(address: string): Promise<any>;
87
89
  /** Private token (registry `metadata.visibility === "private"`); kept per address */
88
90
  isPrivateToken(address: string): Promise<boolean>;
@@ -137,9 +139,12 @@ export class Cx3Wallet {
137
139
  derivedAddress: string;
138
140
  hash: string;
139
141
  }>;
140
- signDeploy(fields: object): Promise<any>;
141
- signSend(contractAddress: string, method: string, params?: object): Promise<any>;
142
- signOwnerAction(action: string, address: string, owner?: string | null): Promise<any>;
142
+ /** A site message the way the CX3Wallet extension and app sign it (0x19 "CX3 Signed Message:\n" prefix) */
143
+ signPersonalMessage(message: string): Promise<any>;
144
+ /** 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) */
145
+ signDeploy(fields: object, opts?: Cx3ChainIdentity): Promise<any>;
146
+ signSend(contractAddress: string, method: string, params?: object, opts?: Cx3ChainIdentity): Promise<any>;
147
+ signOwnerAction(action: string, address: string, owner?: string | null, opts?: Cx3ChainIdentity): Promise<any>;
143
148
  /** A private token's transfer / transferFrom / mint is checked first (PRIVATE_TOKEN_SENDER / _RECIPIENT); `privateCheck: false` skips it */
144
149
  sendTransaction(contractAddress: string, method: string, params?: object, opts?: { wait?: number; async?: boolean; privateCheck?: boolean; [k: string]: unknown }): Promise<TransactionResponse>;
145
150
  /** Throws Cx3Error INVALID_TOKEN_SYMBOL (before signing) for a symbol outside A-Z / 0-9; the chain answers DUPLICATE_TOKEN_SYMBOL (409) for a symbol in use. */
@@ -147,7 +152,7 @@ export class Cx3Wallet {
147
152
  pauseContract(address: string, owner?: string | null): Promise<any>;
148
153
  unpauseContract(address: string, owner?: string | null): Promise<any>;
149
154
  deleteContract(address: string, owner?: string | null): Promise<any>;
150
- transferNative(to: string, amount: number | string, opts?: { nonce?: number; timestamp?: number; data?: unknown; gasLimit?: number; gasPrice?: number; chainId?: string; chainIdBind?: boolean; wait?: number }): Promise<TransactionResponse>;
155
+ transferNative(to: string, amount: number | string, opts?: { nonce?: number; timestamp?: number; data?: unknown; gasLimit?: number; gasPrice?: number; chainId?: string; genesisHash?: string; wait?: number }): Promise<TransactionResponse>;
151
156
  bindApiKey(address?: string): Promise<any>;
152
157
  }
153
158
 
@@ -217,9 +222,18 @@ export function walletApiKeyMode(key?: string | null): "public" | "private" | "i
217
222
  export function toJsonSafe<T>(value: T): unknown;
218
223
  export function ensure0x(hex: string): string;
219
224
  export function strip0x(hex: string): string;
220
- export function buildDeployDigest(fields: object): string;
221
- export function buildSendDigest(contractAddress: string, method: string, params: object, fromAddress: string, timestamp?: number | string): string;
222
- export function buildOwnerActionDigest(action: string, address: string, owner: string, timestamp: number): string;
225
+ /** The network a typed digest or a native transfer is bound to (provider.getChainIdentity()) */
226
+ export interface Cx3ChainIdentity { chainId?: string; genesisHash?: string }
227
+ /** CX3_DEPLOY_V1|[chainId, genesisHash, owner, standard, name, symbol, decimals, initialSupply, timestamp] */
228
+ export function buildDeployDigest(fields: object, identity: Cx3ChainIdentity): string;
229
+ /** CX3_CALL_V1|[chainId, genesisHash, contract, method, params, from, timestamp] */
230
+ export function buildSendDigest(contractAddress: string, method: string, params: object, fromAddress: string, timestamp: number, identity: Cx3ChainIdentity): string;
231
+ /** CX3_OWNER_V1|[chainId, genesisHash, action, address, owner, timestamp] */
232
+ export function buildOwnerActionDigest(action: string, address: string, owner: string, timestamp: number, identity: Cx3ChainIdentity): string;
233
+ /** 0x19 "CX3 Signed Message:\n" + UTF-8 length + message: what a wallet signs for a site message */
234
+ export function personalMessageBytes(message: string): Uint8Array;
235
+ export function signPersonalMessage(message: string, privateKey: string | Uint8Array | Buffer): Promise<any>;
236
+ /** Native TRANSFER payload; fields.chainId and fields.genesisHash are required */
223
237
  export function buildTransferPayload(fields: object): string;
224
238
  export function signMessage(message: string | Uint8Array, privateKey: string | Uint8Array | Buffer): Promise<any>;
225
239
  export function privateKeyToAddress(privateKey: string | Uint8Array | Buffer): string;
package/index.js CHANGED
@@ -18,16 +18,18 @@ const {
18
18
  buildDeployDigest,
19
19
  buildSendDigest,
20
20
  buildOwnerActionDigest,
21
- buildTransferPayload
21
+ buildTransferPayload,
22
+ personalMessageBytes
22
23
  } = require("./src/digest");
23
24
  const {
24
25
  signMessage,
26
+ signPersonalMessage,
25
27
  privateKeyToAddress,
26
28
  createPrivateKey
27
29
  } = require("./src/crypto");
28
30
  const { normalizeAbi, argsToParams } = require("./src/abi");
29
31
 
30
- const VERSION = "0.2.0";
32
+ const VERSION = "0.3.0";
31
33
 
32
34
  /**
33
35
  * Convenience: provider from `options.url` or env `CX3_NODE_URL` — the CX3Api gateway
@@ -62,7 +64,9 @@ module.exports = {
62
64
  buildSendDigest,
63
65
  buildOwnerActionDigest,
64
66
  buildTransferPayload,
67
+ personalMessageBytes,
65
68
  signMessage,
69
+ signPersonalMessage,
66
70
  privateKeyToAddress,
67
71
  createPrivateKey,
68
72
  normalizeAbi,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carbonx3/cx3.js",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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/crypto.js CHANGED
@@ -69,6 +69,16 @@ async function signMessage(message, privateKeyInput) {
69
69
  };
70
70
  }
71
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
+
72
82
  function createPrivateKey() {
73
83
  for (let i = 0; i < 64; i++) {
74
84
  const priv = crypto.randomBytes(32);
@@ -88,5 +98,6 @@ module.exports = {
88
98
  privateKeyToPublicKey,
89
99
  privateKeyToAddress,
90
100
  signMessage,
101
+ signPersonalMessage,
91
102
  createPrivateKey
92
103
  };
package/src/digest.js CHANGED
@@ -1,35 +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
- // Template literal like the node (join() would turn null into "")
18
- const dec = decimals !== undefined ? decimals : 18;
19
- const supply = initialSupply !== undefined ? String(initialSupply) : "0";
20
- return `deploy|${owner || ""}|${standard || ""}|${name || ""}|${symbol || ""}|${dec}|${supply}|${timestamp || 0}`;
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}` : "";
21
22
  }
22
23
 
23
- function buildSendDigest(contractAddress, method, params, fromAddress, timestamp) {
24
- const base = `${contractAddress}|${method}|${JSON.stringify(params || {})}|${fromAddress || ""}`;
25
- if (timestamp !== undefined && timestamp !== null && timestamp !== "") {
26
- 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");
27
29
  }
28
- 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
+ ])}`;
29
39
  }
30
40
 
31
- function buildOwnerActionDigest(action, address, owner, timestamp) {
32
- 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;
33
62
  }
34
63
 
35
64
  function stripNativeAddress(addr) {
@@ -40,12 +69,27 @@ function stripNativeAddress(addr) {
40
69
  return s.toLowerCase();
41
70
  }
42
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
+
43
79
  /**
44
80
  * Canonical native TRANSFER payload — byte-identical to
45
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.
46
85
  */
47
86
  function buildTransferPayload(fields = {}) {
48
- 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({
49
93
  type: fields.type || "TRANSFER",
50
94
  fromAddress: stripNativeAddress(fields.fromAddress),
51
95
  toAddress: stripNativeAddress(fields.toAddress),
@@ -54,15 +98,10 @@ function buildTransferPayload(fields = {}) {
54
98
  timestamp: fields.timestamp,
55
99
  data: fields.data || null,
56
100
  gasLimit: typeof fields.gasLimit === "number" && fields.gasLimit > 0 ? fields.gasLimit : 0,
57
- gasPrice: typeof fields.gasPrice === "number" && fields.gasPrice > 0 ? fields.gasPrice : 0
58
- };
59
- const bind = fields.chainIdBind != null
60
- ? !!fields.chainIdBind
61
- : process.env.CX3_CHAIN_ID_BIND !== "0";
62
- if (bind) {
63
- payload.chainId = String(fields.chainId || process.env.CX3_CHAIN_ID || "cx3-dev");
64
- }
65
- return JSON.stringify(payload);
101
+ gasPrice: typeof fields.gasPrice === "number" && fields.gasPrice > 0 ? fields.gasPrice : 0,
102
+ chainId,
103
+ genesisHash
104
+ });
66
105
  }
67
106
 
68
107
  module.exports = {
@@ -70,5 +109,8 @@ module.exports = {
70
109
  buildSendDigest,
71
110
  buildOwnerActionDigest,
72
111
  buildTransferPayload,
112
+ personalMessageBytes,
113
+ normalizeDigestAddress,
114
+ normalizeGenesisHash,
73
115
  stripNativeAddress
74
116
  };
package/src/provider.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { Cx3Error, Cx3HttpError, Cx3TxError } = require("./errors");
4
4
  const { readCallValue, sleep, normalizeAddress, walletApiKeyMode, toJsonSafe } = require("./utils");
5
5
  const { Contract } = require("./contract");
6
+ const { normalizeGenesisHash } = require("./digest");
6
7
 
7
8
  /**
8
9
  * Green Chain HTTP provider.
@@ -385,6 +386,24 @@ class Cx3Provider {
385
386
  return { success: true, height: blk.index != null ? blk.index : blk.height, hash: blk.hash || null, timestamp: blk.timestamp || null };
386
387
  }
387
388
 
389
+ /**
390
+ * `{ chainId, genesisHash }`: the network identity a native transfer signs over (the gateway's
391
+ * /status, a development node's /chain/identity). Throws NO_CHAIN_IDENTITY when either is missing.
392
+ */
393
+ async getChainIdentity() {
394
+ // changes only with a new genesis: kept for a minute (one request per signature otherwise)
395
+ const hit = this._chainIdentity;
396
+ if (hit && Date.now() - hit.at < 60000) return { ...hit.value };
397
+ const r = this.gateway
398
+ ? this._rejectEnvelope(await this._dappRead("/status"))
399
+ : this._rejectEnvelope(await this.request("/chain/identity"));
400
+ const chainId = typeof r.chainId === "string" ? r.chainId.trim() : "";
401
+ const genesisHash = normalizeGenesisHash(r.genesisHash);
402
+ if (!chainId || !genesisHash) throw new Cx3Error("network identity unavailable", "NO_CHAIN_IDENTITY");
403
+ this._chainIdentity = { at: Date.now(), value: { chainId, genesisHash } };
404
+ return { chainId, genesisHash };
405
+ }
406
+
388
407
  async getEvents(address, typeOrOpts = "", opts = {}) {
389
408
  const extra = (typeOrOpts && typeof typeOrOpts === "object")
390
409
  ? typeOrOpts
package/src/wallet.js CHANGED
@@ -4,6 +4,7 @@ const {
4
4
  privateKeyToAddress,
5
5
  privateKeyToPublicKey,
6
6
  signMessage,
7
+ signPersonalMessage,
7
8
  createPrivateKey
8
9
  } = require("./crypto");
9
10
  const { normalizeAddress, toJsonSafe } = require("./utils");
@@ -46,11 +47,24 @@ class Cx3Wallet {
46
47
  return this.address;
47
48
  }
48
49
 
50
+ /** Sign raw bytes / a digest string as given (sha256 + secp256k1): the typed digests use this. */
49
51
  async signMessage(message) {
50
52
  return signMessage(message, this._privateKey);
51
53
  }
52
54
 
53
- async signDeploy(fields) {
55
+ /** Sign a site message the way the CX3Wallet extension and app do (with the site-message prefix). */
56
+ async signPersonalMessage(message) {
57
+ return signPersonalMessage(message, this._privateKey);
58
+ }
59
+
60
+ /** The network identity typed digests bind: `opts.chainId` + `opts.genesisHash`, else the provider's. */
61
+ async _identity(opts = {}) {
62
+ if (opts.chainId && opts.genesisHash) return { chainId: opts.chainId, genesisHash: opts.genesisHash };
63
+ if (!this.provider) throw new Cx3Error("chainId and genesisHash required without a provider", "NO_CHAIN_IDENTITY");
64
+ return this.provider.getChainIdentity();
65
+ }
66
+
67
+ async signDeploy(fields, opts = {}) {
54
68
  const owner = normalizeAddress(fields.owner || this.address);
55
69
  const timestamp = fields.timestamp || Date.now();
56
70
  const digest = buildDeployDigest({
@@ -61,7 +75,7 @@ class Cx3Wallet {
61
75
  decimals: fields.decimals,
62
76
  initialSupply: fields.initialSupply,
63
77
  timestamp
64
- });
78
+ }, await this._identity(opts));
65
79
  const signed = await this.signMessage(digest);
66
80
  return {
67
81
  ...signed,
@@ -71,11 +85,11 @@ class Cx3Wallet {
71
85
  };
72
86
  }
73
87
 
74
- async signSend(contractAddress, method, params) {
88
+ async signSend(contractAddress, method, params, opts = {}) {
75
89
  const fromAddress = this.address;
76
90
  const timestamp = Date.now();
77
91
  const safeParams = toJsonSafe(params || {});
78
- const digest = buildSendDigest(contractAddress, method, safeParams, fromAddress, timestamp);
92
+ const digest = buildSendDigest(contractAddress, method, safeParams, fromAddress, timestamp, await this._identity(opts));
79
93
  const signed = await this.signMessage(digest);
80
94
  return {
81
95
  ...signed,
@@ -86,10 +100,10 @@ class Cx3Wallet {
86
100
  };
87
101
  }
88
102
 
89
- async signOwnerAction(action, address, owner = null) {
103
+ async signOwnerAction(action, address, owner = null, opts = {}) {
90
104
  const own = normalizeAddress(owner || this.address);
91
105
  const timestamp = Date.now();
92
- const digest = buildOwnerActionDigest(action, address, own, timestamp);
106
+ const digest = buildOwnerActionDigest(action, address, own, timestamp, await this._identity(opts));
93
107
  const signed = await this.signMessage(digest);
94
108
  return {
95
109
  ...signed,
@@ -232,6 +246,10 @@ class Cx3Wallet {
232
246
  nonce = await this.provider.getNonce(this.address);
233
247
  }
234
248
  }
249
+ // the signature covers the network's chainId + genesis hash: asked from the network unless given
250
+ const identity = opts.chainId && opts.genesisHash
251
+ ? { chainId: opts.chainId, genesisHash: opts.genesisHash }
252
+ : await this.provider.getChainIdentity();
235
253
  const timestamp = opts.timestamp || Date.now();
236
254
  const payload = buildTransferPayload({
237
255
  fromAddress: this.address,
@@ -242,8 +260,8 @@ class Cx3Wallet {
242
260
  data: opts.data || null,
243
261
  gasLimit: opts.gasLimit,
244
262
  gasPrice: opts.gasPrice,
245
- chainId: opts.chainId,
246
- chainIdBind: opts.chainIdBind
263
+ chainId: identity.chainId,
264
+ genesisHash: identity.genesisHash
247
265
  });
248
266
  const signed = await this.signMessage(payload);
249
267
  const body = {