@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 +20 -10
- package/index.d.ts +23 -9
- package/index.js +6 -2
- package/package.json +1 -1
- package/src/crypto.js +11 -0
- package/src/digest.js +74 -32
- package/src/provider.js +19 -0
- package/src/wallet.js +26 -8
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:** `
|
|
205
|
-
- **Contract send:** `
|
|
206
|
-
- **Owner action:** `
|
|
207
|
-
- **
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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;
|
|
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
|
-
|
|
221
|
-
export
|
|
222
|
-
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
// 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
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
|
32
|
-
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;
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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:
|
|
246
|
-
|
|
263
|
+
chainId: identity.chainId,
|
|
264
|
+
genesisHash: identity.genesisHash
|
|
247
265
|
});
|
|
248
266
|
const signed = await this.signMessage(payload);
|
|
249
267
|
const body = {
|