@carbonx3/cx3.js 0.3.11 → 0.4.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 +43 -37
- package/index.d.ts +26 -11
- package/index.js +3 -3
- package/package.json +2 -2
- package/src/crypto.js +88 -0
- package/src/provider.js +15 -58
- package/src/utils.js +1 -13
- package/src/wallet.js +44 -5
package/README.md
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
The official Node.js SDK (**CX3js**) for [CarbonX3 Green Chain](https://carbonx3.com/).
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **Developer site:** [CX3Script](https://cx3script.com)
|
|
10
|
+
- **Documentation:** [cx3script.com/docs](https://cx3script.com/docs)
|
|
11
|
+
|
|
12
|
+
CX3js connects server-side applications to CarbonX3 Green Chain through the CX3Api gateway. It signs in as an OAuth2 application, signs transactions with secp256k1 (a key in your process, or your own signer), 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
13
|
|
|
11
14
|
> **Network status:** CarbonX3 Green Chain currently runs as a development network (devnet; `provider.getStatus()` returns its current chain id). Devnet CX3, tokens and contracts are test data with no monetary value and may be cleared when the devnet is reset.
|
|
12
15
|
|
|
@@ -14,6 +17,7 @@ CX3js connects server-side applications to CarbonX3 Green Chain through the CX3A
|
|
|
14
17
|
|
|
15
18
|
- **OAuth2 application access**: client credentials from CX3WebUI; the access token is requested, cached and renewed for you.
|
|
16
19
|
- **Local signing**: private keys stay in your process, and every write carries the wallet's secp256k1 signature.
|
|
20
|
+
- **External signers**: a wallet can sign through an HSM, a KMS, a hardware wallet or a custody service, so the key never enters your process.
|
|
17
21
|
- **Secrets stay out of logs**: a wallet, a provider or a result that is logged or sent as JSON never contains the private key, the client secret or the access token.
|
|
18
22
|
- **Honest results**: a send or transfer the network refuses is reported as failed, and `"confirmed"` always comes with the block it is in.
|
|
19
23
|
- **ABI-bound contracts**: `token.balanceOf(owner)` and `token.transfer(to, amount)`, with the ABI loaded from the chain.
|
|
@@ -87,27 +91,19 @@ ES modules work too: `import { Cx3Provider, Cx3Wallet } from "@carbonx3/cx3.js";
|
|
|
87
91
|
|
|
88
92
|
## Authentication
|
|
89
93
|
|
|
90
|
-
**OAuth2 application
|
|
94
|
+
**OAuth2 application.** The provider exchanges `clientId` and `clientSecret` at `POST /oauth/token` (client credentials grant) for a one-hour access token and sends it as `Authorization: Bearer …`. It renews the token a minute before it expires and, if the gateway answers `invalid_token`, fetches a fresh one and retries once. The secret is sent only to the token endpoint. `scope: ["read"]` requests fewer scopes than the application has. When the options are omitted, the provider reads `CX3_CLIENT_ID` and `CX3_CLIENT_SECRET`.
|
|
91
95
|
|
|
92
96
|
The access token identifies the application; every write is still signed by the wallet. A route outside the application's scopes answers `403 insufficient_scope`, and routes reserved for CarbonX3's own services answer `403 ROUTE_NOT_ALLOWED`. Each application has its own rate limit (300 requests per minute by default, `429 RATE_LIMITED`).
|
|
93
97
|
|
|
94
98
|
`provider.bindApiKey(address)` (or `wallet.bindApiKey()`) confirms that a wallet is the one the application belongs to: `{ ok: true }`, otherwise `wallet_mismatch`.
|
|
95
99
|
|
|
96
|
-
**Wallet API key (legacy).** A `cx3wk_…` key from the same settings page still works while applications move to OAuth2. Pass it as `apiKey` (or set `CX3_API_KEY`); it is sent as `X-Api-Key` and carries every scope of its wallet.
|
|
97
|
-
|
|
98
|
-
```javascript
|
|
99
|
-
const provider = new Cx3Provider("https://cx3api.carbonx3.com", { apiKey: process.env.CX3_API_KEY });
|
|
100
|
-
```
|
|
101
|
-
|
|
102
100
|
**Development node.** `new Cx3Provider(devNodeUrl, { devNode: true })` talks to a development node that runs without authentication (its `/api/v1` routes).
|
|
103
101
|
|
|
104
|
-
**Credentials are required.** Without an application
|
|
105
|
-
|
|
106
|
-
**No HTTP Basic.** HTTP Basic is the internal login between Green Chain nodes and CarbonX3 services, not an application credential, so the SDK never sends it: the `auth` option throws `BASIC_AUTH_UNSUPPORTED`, and `headers` cannot carry `Authorization` or `X-Api-Key`.
|
|
102
|
+
**Credentials are required.** Without an application or `devNode: true`, the constructor throws `CREDENTIALS_REQUIRED`. The application is the only credential: `headers` cannot carry `Authorization`.
|
|
107
103
|
|
|
108
104
|
## Keeping secrets out of logs
|
|
109
105
|
|
|
110
|
-
A wallet's private key and a provider's client secret
|
|
106
|
+
A wallet's private key and a provider's client secret and access token are private fields of their objects. Nothing that prints, serializes or copies an object can reach them:
|
|
111
107
|
|
|
112
108
|
```javascript
|
|
113
109
|
JSON.stringify(wallet); // {"address":"0x…","publicKey":"02…"}
|
|
@@ -118,7 +114,7 @@ const copy = { ...wallet }; // address, publicKey and provider; no key
|
|
|
118
114
|
const key = wallet.exportPrivateKey(); // the one way to read the key back (for example to store a new wallet)
|
|
119
115
|
```
|
|
120
116
|
|
|
121
|
-
`Cx3Wallet.createRandom()` makes a new key in memory only: call `exportPrivateKey()` and store it safely, or the wallet is gone when the process ends. `provider.clearAccessToken()` forgets the held access token, so the next request asks for a new one.
|
|
117
|
+
`Cx3Wallet.createRandom()` makes a new key in memory only: call `exportPrivateKey()` and store it safely, or the wallet is gone when the process ends. `provider.clearAccessToken()` forgets the held access token, so the next request asks for a new one.
|
|
122
118
|
|
|
123
119
|
## Reading chain data
|
|
124
120
|
|
|
@@ -164,6 +160,8 @@ Supported ABI shapes:
|
|
|
164
160
|
|
|
165
161
|
`provider.getContract(address)` loads the ABI the chain calls the contract with: shape 2 for contracts compiled to CX3JS, shape 3 for CX3WASM. Built-in chain standards (ABI inputs `["params"]`) take one named object: `await lp.transfer({ to, amount })`. A contract handle created with a provider instead of a wallet can only read; its writes throw `NO_SIGNER`.
|
|
166
162
|
|
|
163
|
+
ABI methods are properties of a `Contract`, except the names JavaScript calls by itself (`then`, `catch`, `finally`, `toJSON`, `inspect`): a method with such a name is reached with `contract.call(name, params)` or `contract.send(name, params)`.
|
|
164
|
+
|
|
167
165
|
## Transactions and fees
|
|
168
166
|
|
|
169
167
|
Gas is paid in CX3 to the validator that produces the block: gas used × gas price (devnet default 0.0001 CX3 per gas).
|
|
@@ -280,7 +278,7 @@ A private CX31, CX300, CX315 or CX3192 takes its CX3WebUI source as `sourceCode`
|
|
|
280
278
|
|
|
281
279
|
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:
|
|
282
280
|
|
|
283
|
-
- **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.
|
|
281
|
+
- **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.
|
|
284
282
|
- **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.
|
|
285
283
|
- **Owner action:** `CX3_OWNER_V1|` + `JSON.stringify([chainId, genesisHash, action, address, owner, timestamp])`
|
|
286
284
|
- **Site message:** `0x19` + `"CX3 Signed Message:\n"` + UTF-8 length + message (`signPersonalMessage`), the way the CX3Wallet extension and app sign `cx3_sign_message`
|
|
@@ -288,7 +286,30 @@ Digests are UTF-8 strings, hashed with SHA-256 and signed with secp256k1, byte-i
|
|
|
288
286
|
|
|
289
287
|
BigInt values in params are converted to decimal strings before signing, and exactly those params are sent. Deploy, contract-send and owner signatures carry a timestamp and are accepted within ±5 minutes of the gateway's clock. A native transfer is admitted only within 60 seconds after its timestamp (the pool drops it after 60 seconds), and a block takes it only 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)`.
|
|
290
288
|
|
|
291
|
-
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`.
|
|
289
|
+
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`, `publicKeyToAddress` and `createPrivateKey`.
|
|
290
|
+
|
|
291
|
+
`wallet.signPersonalMessage(text)` is for any text that comes from outside (a login challenge, a user's message). `wallet.signMessage` signs a text as it is and refuses one the chain would accept as a transfer, contract call, deploy or owner action (`SIGN_REFUSED_TRANSACTION_PAYLOAD`), so a service that signs messages for its users cannot be made to sign a payment.
|
|
292
|
+
|
|
293
|
+
### External signers
|
|
294
|
+
|
|
295
|
+
`Cx3Wallet.fromSigner()` makes a wallet whose key stays outside your process: in an HSM, a cloud KMS, a hardware wallet or a custody service. You give the public key and a `sign` function; everything else (`transferNative`, contract sends, deploys, `signPersonalMessage`) works as with a local key.
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
const wallet = Cx3Wallet.fromSigner({
|
|
299
|
+
publicKey: process.env.CX3_PUBLIC_KEY, // compressed (33 bytes) or uncompressed (65 bytes), hex or bytes
|
|
300
|
+
async sign(hash, info) {
|
|
301
|
+
// hash: the 32-byte SHA-256 the chain verifies (Uint8Array)
|
|
302
|
+
// info.kind: "transaction" | "personalMessage" | "message"; info.payload: what the hash is of
|
|
303
|
+
return myKms.signSecp256k1(hash); // r || s (64 bytes, hex or bytes) or { r, s }
|
|
304
|
+
}
|
|
305
|
+
}, provider);
|
|
306
|
+
|
|
307
|
+
await wallet.transferNative("0xRecipient…", 1.5);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
- `info.payload` is the exact text being signed (the typed digest or the transfer's JSON), so the signer can show it or check it against a policy before it signs.
|
|
311
|
+
- The SDK checks every answer against the wallet's public key before it is used: a signature of another key or over another hash throws `SIGNER_BAD_SIGNATURE` and nothing is sent. A signature with a high `s` is normalized to the low form the chain expects.
|
|
312
|
+
- An error thrown by `sign` surfaces as `SIGNER_FAILED`. `wallet.external` is `true`, and `exportPrivateKey()` throws `NO_PRIVATE_KEY`: there is no key to export.
|
|
292
313
|
|
|
293
314
|
## Error handling
|
|
294
315
|
|
|
@@ -307,7 +328,7 @@ try {
|
|
|
307
328
|
|
|
308
329
|
| Code | Meaning |
|
|
309
330
|
|------|---------|
|
|
310
|
-
| `CREDENTIALS_REQUIRED` | No application
|
|
331
|
+
| `CREDENTIALS_REQUIRED` | No application or `devNode` was given |
|
|
311
332
|
| `INVALID_CLIENT_CREDENTIALS` | The client ID or secret is not in the `cx3app_…` / `cx3sec_…` format |
|
|
312
333
|
| `invalid_client` | The token endpoint refused the application (wrong secret, or revoked) |
|
|
313
334
|
| `insufficient_scope`, `ROUTE_NOT_ALLOWED` | The route needs another scope, or is not open to applications (403) |
|
|
@@ -332,46 +353,31 @@ try {
|
|
|
332
353
|
| `REPLAYED_REQUEST` | This signed request was already received or already ran (409); the answer does not say how it ended. Look it up with `provider.getRequest(err.body.requestId, wallet.address)` and never sign it again |
|
|
333
354
|
| `REQUEST_LOOKUP_UNAVAILABLE` | `getRequest` could not ask the chain (503): try again later, do not sign again |
|
|
334
355
|
| `LEDGER_UNAVAILABLE`, `LEDGER_FULL` | The gateway's `/dapp/v1` send could not record the request (503): it was not sent, and the same signed body may be sent again later, without a new signature |
|
|
335
|
-
| `
|
|
356
|
+
| `INVALID_SIGNER`, `INVALID_PUBLIC_KEY` | `Cx3Wallet.fromSigner` was given no `sign` function, or a public key that is not a secp256k1 point |
|
|
357
|
+
| `SIGNER_FAILED`, `SIGNER_BAD_SIGNATURE` | An external signer threw, or returned a signature that is not this wallet's over this hash; nothing is sent |
|
|
358
|
+
| `NO_PRIVATE_KEY` | `exportPrivateKey()` on a wallet that signs with an external signer |
|
|
336
359
|
| `NO_CHAIN_IDENTITY` | The network's chain id or genesis hash is unavailable; a native transfer is not signed without them |
|
|
337
360
|
| `CHAIN_IDENTITY_MISMATCH` | The provider was made for one network (`chainId` and `genesisHash` options) and the endpoint names another; nothing is signed (`err.details.expected`, `err.details.got`) |
|
|
338
361
|
| `INSECURE_BASE_URL` | Credentials would travel over plain `http` to another machine; use `https` (or `allowInsecure: true` on a network you trust) |
|
|
339
362
|
|
|
340
363
|
## Browser applications
|
|
341
364
|
|
|
342
|
-
This package is for Node.js servers. Web pages use the **CX3 IIFE** (`window.cx3`) together with the CX3Wallet extension: [`@carbonx3/cx3-iife`](https://www.npmjs.com/package/@carbonx3/cx3-iife) on npm and jsDelivr, or [download it](https://cx3script.com/download/cx3-0.2.11.min.js); see the [CX3 IIFE guide](https://cx3script.com/docs?doc=cx3iife). Keep the application's credentials on your server behind a same-origin proxy, and never put a client secret
|
|
365
|
+
This package is for Node.js servers. Web pages use the **CX3 IIFE** (`window.cx3`) together with the CX3Wallet extension: [`@carbonx3/cx3-iife`](https://www.npmjs.com/package/@carbonx3/cx3-iife) on npm and jsDelivr, or [download it](https://cx3script.com/download/cx3-0.2.11.min.js); see the [CX3 IIFE guide](https://cx3script.com/docs?doc=cx3iife). Keep the application's credentials on your server behind a same-origin proxy, and never put a client secret or a private key in browser JavaScript.
|
|
343
366
|
|
|
344
367
|
## Environment variables
|
|
345
368
|
|
|
346
369
|
| Variable | Used for |
|
|
347
370
|
|----------|----------|
|
|
348
371
|
| `CX3_CLIENT_ID`, `CX3_CLIENT_SECRET` | Application credentials when the options are omitted (not read with `devNode: true`) |
|
|
349
|
-
| `CX3_API_KEY` | Legacy wallet API key |
|
|
350
372
|
| `CX3_NODE_URL` | Gateway URL for `getDefaultProvider()` |
|
|
351
373
|
|
|
352
374
|
## TypeScript
|
|
353
375
|
|
|
354
376
|
Type declarations (`index.d.ts`) ship with the package.
|
|
355
377
|
|
|
356
|
-
##
|
|
357
|
-
|
|
358
|
-
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.10.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`). In 0.3.10, a refused native transfer (HTTP 200 with `error`) is `success: false` / `status: "failed"` and `wait()` rejects (before, it was reported as confirmed); an accepted transfer without a block is `"pending"`; `wait()` rejects with `NO_TX_HASH` when the answer names no transaction; the private key, client secret, API key and access token are private fields (`wallet._privateKey`, `provider.apiKey` and `provider._clientSecret` are gone; use `wallet.exportPrivateKey()`), and results no longer carry the provider into JSON or logs. In 0.3.11, `transferNative` refuses a recipient that is not a full address (`INVALID_RECIPIENT`) and a text amount that is not plain decimal digits; `sendTransaction`, `deployContract` and the owner actions sign for `opts.chainId` and `opts.genesisHash` when both are given; a provider can be made for one network (`chainId` and `genesisHash` options, `CHAIN_IDENTITY_MISMATCH`); credentials are not sent over plain `http` to another machine (`INSECURE_BASE_URL`); with `devNode: true` the credentials of the environment are not read; and `@noble/secp256k1` is pinned to one exact version.
|
|
359
|
-
|
|
360
|
-
- **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.
|
|
361
|
-
- **No fallback chain id.** `buildTransferPayload` throws `NO_CHAIN_IDENTITY` without `chainId` and `genesisHash`; the `CX3_CHAIN_ID` variable and the `chainIdBind` option are gone.
|
|
362
|
-
- **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.
|
|
363
|
-
- **Site messages.** `signPersonalMessage` signs the way the wallets sign `cx3_sign_message` (with the `0x19` prefix). Use it for any text that comes from outside (a login challenge, a user's message). `wallet.signMessage` signs a text as it is, without the prefix, and refuses a text the chain would accept as a transfer, contract call, deploy or owner action (`SIGN_REFUSED_TRANSACTION_PAYLOAD`), so a service that signs messages for its users cannot be made to sign a payment. The exported `signMessage(message, privateKey)` function is the low-level form without that check.
|
|
364
|
-
- **Contract method names.** ABI methods are properties of a `Contract`, except the names JavaScript calls by itself (`then`, `catch`, `finally`, `toJSON`, `inspect`): a contract with such a method is reached with `contract.call(name, params)` or `contract.send(name, params)`.
|
|
365
|
-
|
|
366
|
-
## Upgrading from 0.1.x
|
|
367
|
-
|
|
368
|
-
CX3js is in beta (0.x), so a minor version may contain breaking changes. Version 0.2.0:
|
|
378
|
+
## Versioning
|
|
369
379
|
|
|
370
|
-
|
|
371
|
-
- **HTTP Basic was removed.** The `auth` option throws `BASIC_AUTH_UNSUPPORTED`, and `headers` cannot carry `Authorization` or `X-Api-Key`.
|
|
372
|
-
- **Node.js 20.19 or later** is required (`@noble/secp256k1` 3.x).
|
|
373
|
-
- **Fixes:** BigInt params are signed and sent as decimal strings, and `transferNative` sends the gas fields it signs.
|
|
374
|
-
- **New:** public chain reads (`getStatus`, `getTokens`, `getActivity`, `getDexPairs`, `getLpBalance`, `getWit`), private tokens and `isValidTokenSymbol`.
|
|
380
|
+
CX3js is in beta (0.x): a minor version may contain breaking changes, a patch version does not. Version 0.4.0 adds external signers (`Cx3Wallet.fromSigner`), makes the OAuth2 application the SDK's only credential.
|
|
375
381
|
|
|
376
382
|
## Documentation
|
|
377
383
|
|
package/index.d.ts
CHANGED
|
@@ -29,8 +29,6 @@ export interface Cx3ProviderOptions {
|
|
|
29
29
|
clientSecret?: string;
|
|
30
30
|
/** ask for fewer scopes than the app has: "read", "write", "deploy" */
|
|
31
31
|
scope?: string | Array<"read" | "write" | "deploy">;
|
|
32
|
-
/** wallet API key (cx3wk_…), until apps move to OAuth2; env CX3_API_KEY */
|
|
33
|
-
apiKey?: string;
|
|
34
32
|
/** a development node that runs without auth (/api/v1); the credentials of the environment are not read for it */
|
|
35
33
|
devNode?: boolean;
|
|
36
34
|
/** The network this provider is for (both together): an endpoint on another network is refused with CHAIN_IDENTITY_MISMATCH and nothing is signed */
|
|
@@ -40,9 +38,7 @@ export interface Cx3ProviderOptions {
|
|
|
40
38
|
allowInsecure?: boolean;
|
|
41
39
|
/** "" on the gateway, "/api/v1" on a development node */
|
|
42
40
|
apiPrefix?: string;
|
|
43
|
-
/**
|
|
44
|
-
auth?: never;
|
|
45
|
-
/** extra headers; Authorization and X-Api-Key are dropped (use clientId / clientSecret or apiKey) */
|
|
41
|
+
/** extra headers; Authorization and X-Api-Key are dropped (the application is the only credential) */
|
|
46
42
|
headers?: Record<string, string>;
|
|
47
43
|
fetch?: typeof fetch;
|
|
48
44
|
timeoutMs?: number;
|
|
@@ -78,14 +74,13 @@ export class Cx3Provider {
|
|
|
78
74
|
devNode: boolean;
|
|
79
75
|
oauth: boolean;
|
|
80
76
|
clientId: string;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/** Throws CREDENTIALS_REQUIRED without clientId / clientSecret, apiKey or devNode */
|
|
77
|
+
authMode: "oauth" | "devNode";
|
|
78
|
+
/** Throws CREDENTIALS_REQUIRED without clientId / clientSecret or devNode */
|
|
84
79
|
constructor(baseUrl: string, options?: Cx3ProviderOptions);
|
|
85
80
|
/** Forget the held access token: the next request asks for a new one */
|
|
86
81
|
clearAccessToken(): void;
|
|
87
82
|
/** Where the provider points and how it signs in; never a credential (JSON.stringify and util.inspect use it) */
|
|
88
|
-
toJSON(): { baseUrl: string; authMode: "oauth" | "
|
|
83
|
+
toJSON(): { baseUrl: string; authMode: "oauth" | "devNode"; clientId?: string; scope?: string };
|
|
89
84
|
/** The current OAuth2 access token (renewed a minute before it expires); null without an OAuth2 app */
|
|
90
85
|
getAccessToken(force?: boolean): Promise<string | null>;
|
|
91
86
|
request(path: string, opts?: { method?: string; body?: unknown; query?: Record<string, string | number> }): Promise<any>;
|
|
@@ -117,6 +112,7 @@ export class Cx3Provider {
|
|
|
117
112
|
getBalance(address: string): Promise<any>;
|
|
118
113
|
getNonce(address: string): Promise<number>;
|
|
119
114
|
getPendingNonce(address: string): Promise<any>;
|
|
115
|
+
/** Is this wallet the one the application belongs to: { ok: true }, or wallet_mismatch */
|
|
120
116
|
bindApiKey(address: string): Promise<any>;
|
|
121
117
|
/** The gateway lists a private token only when the app's wallet is its owner or an authorized wallet */
|
|
122
118
|
listContracts(opts?: { limit?: number; offset?: number; viewer?: string; owner?: string; standard?: string; type?: string; live?: boolean }): Promise<any>;
|
|
@@ -136,14 +132,32 @@ export class Cx3Provider {
|
|
|
136
132
|
getContract(address: string, abi?: unknown, signer?: Cx3Wallet | null): Promise<Contract>;
|
|
137
133
|
}
|
|
138
134
|
|
|
135
|
+
/** What an external signer is asked to sign: the kind of request and the exact text (or bytes) the hash is of */
|
|
136
|
+
export interface Cx3SignInfo {
|
|
137
|
+
kind: "transaction" | "personalMessage" | "message";
|
|
138
|
+
payload: string | Uint8Array;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A key kept outside the process (HSM, KMS, hardware wallet, custody service) */
|
|
142
|
+
export interface Cx3Signer {
|
|
143
|
+
/** secp256k1 public key, compressed (33 bytes) or uncompressed (65 bytes), hex or bytes */
|
|
144
|
+
publicKey: string | Uint8Array;
|
|
145
|
+
/** Signs the 32-byte SHA-256 `hash`; returns r || s (64 bytes, hex or bytes) or { r, s } (hex) */
|
|
146
|
+
sign(hash: Uint8Array, info: Cx3SignInfo): Promise<string | Uint8Array | { r: string; s: string }> | string | Uint8Array | { r: string; s: string };
|
|
147
|
+
}
|
|
148
|
+
|
|
139
149
|
export class Cx3Wallet {
|
|
140
150
|
address: string;
|
|
141
151
|
publicKey: string;
|
|
142
152
|
provider: Cx3Provider | null;
|
|
153
|
+
/** true for a wallet made with fromSigner (no private key in this process) */
|
|
154
|
+
external: boolean;
|
|
143
155
|
constructor(privateKey: string | Uint8Array | Buffer, provider?: Cx3Provider | null);
|
|
144
156
|
static createRandom(provider?: Cx3Provider | null): Cx3Wallet;
|
|
157
|
+
/** A wallet that signs through `signer`; every answer is checked against the public key (SIGNER_BAD_SIGNATURE) */
|
|
158
|
+
static fromSigner(signer: Cx3Signer, provider?: Cx3Provider | null): Cx3Wallet;
|
|
145
159
|
connect(provider: Cx3Provider): Cx3Wallet;
|
|
146
|
-
/** The private key as given to the constructor; the wallet shows it nowhere else (not in JSON, logs or a spread) */
|
|
160
|
+
/** The private key as given to the constructor; the wallet shows it nowhere else (not in JSON, logs or a spread). Throws NO_PRIVATE_KEY on an external-signer wallet */
|
|
147
161
|
exportPrivateKey(): string | Uint8Array | Buffer;
|
|
148
162
|
/** Address and public key only (JSON.stringify and util.inspect use it) */
|
|
149
163
|
toJSON(): { address: string; publicKey: string };
|
|
@@ -295,7 +309,6 @@ export class TransactionResponse {
|
|
|
295
309
|
export function getDefaultProvider(options?: Cx3ProviderOptions & { url?: string }): Cx3Provider;
|
|
296
310
|
export function readCallValue(response: any): unknown;
|
|
297
311
|
export function normalizeAddress(addr: string): string;
|
|
298
|
-
export function walletApiKeyMode(key?: string | null): "public" | "private" | "invalid";
|
|
299
312
|
/** BigInt → decimal string, recursively (params are signed and sent in this form) */
|
|
300
313
|
export function toJsonSafe<T>(value: T): unknown;
|
|
301
314
|
export function ensure0x(hex: string): string;
|
|
@@ -317,6 +330,8 @@ export function signPersonalMessage(message: string, privateKey: string | Uint8A
|
|
|
317
330
|
export function buildTransferPayload(fields: object): string;
|
|
318
331
|
export function signMessage(message: string | Uint8Array, privateKey: string | Uint8Array | Buffer): Promise<any>;
|
|
319
332
|
export function privateKeyToAddress(privateKey: string | Uint8Array | Buffer): string;
|
|
333
|
+
/** The address of a secp256k1 public key (compressed or uncompressed, hex or bytes) */
|
|
334
|
+
export function publicKeyToAddress(publicKey: string | Uint8Array | Buffer): string;
|
|
320
335
|
export function createPrivateKey(): string;
|
|
321
336
|
export function normalizeAbi(abi: unknown): Record<string, any>;
|
|
322
337
|
export function argsToParams(inputs: any[], args: any[]): object;
|
package/index.js
CHANGED
|
@@ -15,7 +15,6 @@ const {
|
|
|
15
15
|
normalizeAddress,
|
|
16
16
|
ensure0x,
|
|
17
17
|
strip0x,
|
|
18
|
-
walletApiKeyMode,
|
|
19
18
|
toJsonSafe
|
|
20
19
|
} = require("./src/utils");
|
|
21
20
|
const {
|
|
@@ -30,11 +29,12 @@ const {
|
|
|
30
29
|
signMessage,
|
|
31
30
|
signPersonalMessage,
|
|
32
31
|
privateKeyToAddress,
|
|
32
|
+
publicKeyToAddress,
|
|
33
33
|
createPrivateKey
|
|
34
34
|
} = require("./src/crypto");
|
|
35
35
|
const { normalizeAbi, argsToParams } = require("./src/abi");
|
|
36
36
|
|
|
37
|
-
const VERSION = "0.
|
|
37
|
+
const VERSION = "0.4.0";
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Convenience: provider from `options.url` or env `CX3_NODE_URL` — the CX3Api gateway
|
|
@@ -63,7 +63,6 @@ module.exports = {
|
|
|
63
63
|
normalizeAddress,
|
|
64
64
|
ensure0x,
|
|
65
65
|
strip0x,
|
|
66
|
-
walletApiKeyMode,
|
|
67
66
|
toJsonSafe,
|
|
68
67
|
buildDeployDigest,
|
|
69
68
|
deployContentText,
|
|
@@ -74,6 +73,7 @@ module.exports = {
|
|
|
74
73
|
signMessage,
|
|
75
74
|
signPersonalMessage,
|
|
76
75
|
privateKeyToAddress,
|
|
76
|
+
publicKeyToAddress,
|
|
77
77
|
createPrivateKey,
|
|
78
78
|
normalizeAbi,
|
|
79
79
|
argsToParams,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carbonx3/cx3.js",
|
|
3
|
-
"version": "0.
|
|
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",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Official Node.js SDK (CX3js) for CarbonX3 Green Chain: OAuth2 app access, local or external secp256k1 signing, ABI-bound contracts and token deploys over REST + JSON",
|
|
5
5
|
"homepage": "https://cx3script.com",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
package/src/crypto.js
CHANGED
|
@@ -73,6 +73,91 @@ async function signMessage(message, privateKeyInput) {
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** secp256k1 group order: a signature's s is kept in the lower half (the form the chain and the wallets produce). */
|
|
77
|
+
const CURVE_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
78
|
+
|
|
79
|
+
function bytesOf(input, what, code) {
|
|
80
|
+
if (input instanceof Uint8Array || Buffer.isBuffer(input)) return Buffer.from(input);
|
|
81
|
+
const hex = strip0x(String(input == null ? "" : input)).toLowerCase();
|
|
82
|
+
if (!hex || hex.length % 2 || !/^[0-9a-f]+$/.test(hex)) throw new Cx3Error(`Invalid ${what}`, code);
|
|
83
|
+
return Buffer.from(hex, "hex");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Both forms of a public key given compressed (33 bytes) or uncompressed (65 bytes), hex or bytes. */
|
|
87
|
+
function publicKeyForms(input) {
|
|
88
|
+
const bytes = bytesOf(input, "public key", "INVALID_PUBLIC_KEY");
|
|
89
|
+
if (bytes.length !== 33 && bytes.length !== 65) throw new Cx3Error("Invalid public key length", "INVALID_PUBLIC_KEY");
|
|
90
|
+
let point;
|
|
91
|
+
try {
|
|
92
|
+
point = secp.Point.fromBytes(bytes);
|
|
93
|
+
point.assertValidity();
|
|
94
|
+
} catch (_) {
|
|
95
|
+
throw new Cx3Error("Invalid public key", "INVALID_PUBLIC_KEY");
|
|
96
|
+
}
|
|
97
|
+
return { compressed: Buffer.from(point.toBytes(true)), uncompressed: Buffer.from(point.toBytes(false)) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function publicKeyToAddress(publicKeyInput) {
|
|
101
|
+
return ensure0x(sha256(publicKeyForms(publicKeyInput).uncompressed).slice(-20).toString("hex"));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** What a signer returned, as 64 bytes r || s with s in the lower half: 64 bytes (hex or bytes) or { r, s } (hex). */
|
|
105
|
+
function compactSignature(out) {
|
|
106
|
+
let bytes;
|
|
107
|
+
if (out && typeof out === "object" && !(out instanceof Uint8Array) && out.r != null && out.s != null) {
|
|
108
|
+
const part = (v) => strip0x(String(v)).toLowerCase().padStart(64, "0");
|
|
109
|
+
bytes = bytesOf(part(out.r) + part(out.s), "signature", "SIGNER_BAD_SIGNATURE");
|
|
110
|
+
} else {
|
|
111
|
+
bytes = bytesOf(out, "signature", "SIGNER_BAD_SIGNATURE");
|
|
112
|
+
}
|
|
113
|
+
if (bytes.length !== 64) throw new Cx3Error("The signer must return 64 bytes (r || s)", "SIGNER_BAD_SIGNATURE");
|
|
114
|
+
const r = BigInt("0x" + bytes.subarray(0, 32).toString("hex"));
|
|
115
|
+
let s = BigInt("0x" + bytes.subarray(32).toString("hex"));
|
|
116
|
+
if (r <= 0n || r >= CURVE_N || s <= 0n || s >= CURVE_N) throw new Cx3Error("Signature out of range", "SIGNER_BAD_SIGNATURE");
|
|
117
|
+
if (s > CURVE_N / 2n) s = CURVE_N - s;
|
|
118
|
+
return Buffer.from(r.toString(16).padStart(64, "0") + s.toString(16).padStart(64, "0"), "hex");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* signMessage() with a signer that keeps the key elsewhere (an HSM, a KMS, a hardware wallet, a custody
|
|
123
|
+
* service). `signer.sign(hash, info)` gets the 32-byte sha256 the chain verifies and what it is the hash
|
|
124
|
+
* of; the answer is checked against the wallet's public key before anything is sent with it.
|
|
125
|
+
*/
|
|
126
|
+
async function signWithSigner(message, signer, kind) {
|
|
127
|
+
const msgBytes = typeof message === "string" ? Buffer.from(message, "utf8") : Buffer.from(message);
|
|
128
|
+
const msgHash = sha256(msgBytes);
|
|
129
|
+
let out;
|
|
130
|
+
try {
|
|
131
|
+
out = await signer.sign(new Uint8Array(msgHash), { kind, payload: typeof message === "string" ? message : new Uint8Array(msgBytes) });
|
|
132
|
+
} catch (e) {
|
|
133
|
+
if (e instanceof Cx3Error) throw e;
|
|
134
|
+
throw new Cx3Error("The signer did not sign: " + String((e && e.message) || e).slice(0, 200), "SIGNER_FAILED");
|
|
135
|
+
}
|
|
136
|
+
const compact = compactSignature(out);
|
|
137
|
+
let recovery = -1;
|
|
138
|
+
for (const bit of [0, 1]) {
|
|
139
|
+
try {
|
|
140
|
+
const got = secp.recoverPublicKey(Buffer.concat([Buffer.from([bit]), compact]), msgHash, { prehash: false, isCompressed: false });
|
|
141
|
+
if (Buffer.from(got).equals(signer.uncompressed)) { recovery = bit; break; }
|
|
142
|
+
} catch (_) { /* not this bit */ }
|
|
143
|
+
}
|
|
144
|
+
// a signature of another key, or over another hash, never leaves the SDK
|
|
145
|
+
if (recovery < 0) throw new Cx3Error("The signer's signature is not this wallet's", "SIGNER_BAD_SIGNATURE");
|
|
146
|
+
const signature = compact.toString("hex");
|
|
147
|
+
const publicKey = signer.compressed.toString("hex");
|
|
148
|
+
return {
|
|
149
|
+
signature,
|
|
150
|
+
r: signature,
|
|
151
|
+
s: null,
|
|
152
|
+
v: recovery + 27,
|
|
153
|
+
publicKey,
|
|
154
|
+
senderPublicKey: publicKey,
|
|
155
|
+
publicKeyUncompressed: signer.uncompressed.toString("hex"),
|
|
156
|
+
hash: ensure0x(msgHash.toString("hex")),
|
|
157
|
+
derivedAddress: ensure0x(sha256(signer.uncompressed).slice(-20).toString("hex"))
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
76
161
|
/**
|
|
77
162
|
* Sign a site message the way the CX3Wallet extension and app do (cx3_sign_message):
|
|
78
163
|
* sha256(0x19 "CX3 Signed Message:\n" + UTF-8 length + message). Such a signature never verifies as
|
|
@@ -103,5 +188,8 @@ module.exports = {
|
|
|
103
188
|
privateKeyToAddress,
|
|
104
189
|
signMessage,
|
|
105
190
|
signPersonalMessage,
|
|
191
|
+
signWithSigner,
|
|
192
|
+
publicKeyForms,
|
|
193
|
+
publicKeyToAddress,
|
|
106
194
|
createPrivateKey
|
|
107
195
|
};
|
package/src/provider.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"use strict";
|
|
6
6
|
|
|
7
7
|
const { Cx3Error, Cx3HttpError, Cx3TxError } = require("./errors");
|
|
8
|
-
const { readCallValue, sleep, normalizeAddress,
|
|
8
|
+
const { readCallValue, sleep, normalizeAddress, toJsonSafe } = require("./utils");
|
|
9
9
|
const { Contract } = require("./contract");
|
|
10
10
|
const { normalizeGenesisHash } = require("./digest");
|
|
11
11
|
|
|
@@ -14,12 +14,11 @@ const { normalizeGenesisHash } = require("./digest");
|
|
|
14
14
|
*
|
|
15
15
|
* Apps sign in to the CX3Api gateway with an OAuth2 client (`clientId` + `clientSecret`, created in
|
|
16
16
|
* CX3WebUI → Settings → API → Applications): the provider gets a one-hour access token at
|
|
17
|
-
* `POST /oauth/token`, sends it as `Authorization: Bearer`, and renews it before it expires.
|
|
18
|
-
*
|
|
19
|
-
* two the provider refuses to start (CREDENTIALS_REQUIRED): no app development without credentials.
|
|
17
|
+
* `POST /oauth/token`, sends it as `Authorization: Bearer`, and renews it before it expires. Without
|
|
18
|
+
* an application the provider refuses to start (CREDENTIALS_REQUIRED).
|
|
20
19
|
* Gateway routes: `/contracts/:address/call`, `/contracts/:address/send`, `/wallet/...`, …
|
|
21
|
-
* `devNode: true` talks to a development node that runs without auth (`/api/v1`).
|
|
22
|
-
*
|
|
20
|
+
* `devNode: true` talks to a development node that runs without auth (`/api/v1`). Never talks to
|
|
21
|
+
* CX3VM directly.
|
|
23
22
|
*/
|
|
24
23
|
|
|
25
24
|
// node route → CX3Api route (the gateway forwards the same body to the node)
|
|
@@ -56,7 +55,7 @@ function boolVerdict(v) {
|
|
|
56
55
|
return null;
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
// credentials come only from clientId / clientSecret
|
|
58
|
+
// credentials come only from clientId / clientSecret, never from an extra header
|
|
60
59
|
function withoutCredentialHeaders(headers) {
|
|
61
60
|
const out = {};
|
|
62
61
|
for (const [k, v] of Object.entries(headers || {})) {
|
|
@@ -90,7 +89,6 @@ class Cx3Provider {
|
|
|
90
89
|
// Credentials live in private fields: JSON.stringify, util.inspect, console.log, Object.keys and a
|
|
91
90
|
// spread of the provider (or of anything that holds it) never show them.
|
|
92
91
|
#clientSecret = "";
|
|
93
|
-
#apiKey = "";
|
|
94
92
|
#token = null; // { value, refreshAt, scope }
|
|
95
93
|
|
|
96
94
|
/**
|
|
@@ -99,7 +97,6 @@ class Cx3Provider {
|
|
|
99
97
|
* @param {string} [options.clientId] OAuth2 client id (`cx3app_…`; env CX3_CLIENT_ID)
|
|
100
98
|
* @param {string} [options.clientSecret] OAuth2 client secret (`cx3sec_…`; env CX3_CLIENT_SECRET)
|
|
101
99
|
* @param {string|string[]} [options.scope] ask for fewer scopes than the app has ("read", "write", "deploy")
|
|
102
|
-
* @param {string} [options.apiKey] wallet API key (`cx3wk_…`; env CX3_API_KEY), until apps move to OAuth2
|
|
103
100
|
* @param {boolean} [options.devNode] a development node without auth (`/api/v1`), no credentials
|
|
104
101
|
* @param {string} [options.apiPrefix] ("" on the gateway, "/api/v1" on a development node)
|
|
105
102
|
* @param {Record<string,string>} [options.headers] Extra headers (Authorization / X-Api-Key are dropped)
|
|
@@ -109,32 +106,24 @@ class Cx3Provider {
|
|
|
109
106
|
constructor(baseUrl, options = {}) {
|
|
110
107
|
if (!baseUrl) throw new Cx3Error("Cx3Provider requires baseUrl (CX3_NODE_URL)", "NO_BASE_URL");
|
|
111
108
|
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
112
|
-
if (options.auth != null) {
|
|
113
|
-
throw new Cx3Error(
|
|
114
|
-
"HTTP Basic is the nodes' internal login and is not sent by the SDK: create an application in CX3WebUI (Settings → API) and pass clientId / clientSecret",
|
|
115
|
-
"BASIC_AUTH_UNSUPPORTED"
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
109
|
this.devNode = options.devNode === true;
|
|
119
110
|
// a development node gets only what the caller passes: the gateway credentials of the environment never go to it
|
|
120
111
|
const env = (name) => (this.devNode ? "" : (process.env[name] || "").trim());
|
|
121
112
|
this.clientId = String(options.clientId != null ? options.clientId : env("CX3_CLIENT_ID")).trim();
|
|
122
113
|
this.#clientSecret = String(options.clientSecret != null ? options.clientSecret : env("CX3_CLIENT_SECRET")).trim();
|
|
123
114
|
this.scope = Array.isArray(options.scope) ? options.scope.join(" ") : String(options.scope || "").trim();
|
|
124
|
-
this.#apiKey = String(options.apiKey != null ? options.apiKey : env("CX3_API_KEY")).trim();
|
|
125
115
|
this.oauth = !!(this.clientId || this.#clientSecret);
|
|
126
116
|
if (this.oauth && (!CLIENT_ID_RE.test(this.clientId) || !CLIENT_SECRET_RE.test(this.#clientSecret))) {
|
|
127
117
|
throw new Cx3Error("clientId must be cx3app_… and clientSecret cx3sec_… (CX3WebUI → Settings → API → Applications)", "INVALID_CLIENT_CREDENTIALS");
|
|
128
118
|
}
|
|
129
|
-
|
|
130
|
-
if (!this.oauth && !hasKey && !this.devNode) {
|
|
119
|
+
if (!this.oauth && !this.devNode) {
|
|
131
120
|
throw new Cx3Error(
|
|
132
121
|
"App credentials required: create an application in CX3WebUI (Settings → API → Applications) and pass clientId / clientSecret (or set CX3_CLIENT_ID / CX3_CLIENT_SECRET)",
|
|
133
122
|
"CREDENTIALS_REQUIRED"
|
|
134
123
|
);
|
|
135
124
|
}
|
|
136
125
|
// credentials over plain http can be read on the way: only this machine, or the caller's own word
|
|
137
|
-
if (
|
|
126
|
+
if (this.oauth && options.allowInsecure !== true && isRemotePlainHttp(this.baseUrl)) {
|
|
138
127
|
throw new Cx3Error(
|
|
139
128
|
"Credentials are not sent over plain http: use an https URL (or allowInsecure: true on a network you trust)",
|
|
140
129
|
"INSECURE_BASE_URL"
|
|
@@ -164,14 +153,9 @@ class Cx3Provider {
|
|
|
164
153
|
}
|
|
165
154
|
}
|
|
166
155
|
|
|
167
|
-
|
|
168
|
-
return walletApiKeyMode(this.#apiKey);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** "oauth" | "apiKey" | "devNode" */
|
|
156
|
+
/** "oauth" | "devNode" */
|
|
172
157
|
get authMode() {
|
|
173
|
-
|
|
174
|
-
return this.oauth ? "oauth" : "apiKey";
|
|
158
|
+
return this.oauth ? "oauth" : "devNode";
|
|
175
159
|
}
|
|
176
160
|
|
|
177
161
|
/**
|
|
@@ -223,11 +207,7 @@ class Cx3Provider {
|
|
|
223
207
|
"Content-Type": "application/json",
|
|
224
208
|
...this.extraHeaders
|
|
225
209
|
};
|
|
226
|
-
if (this.oauth) {
|
|
227
|
-
headers.Authorization = `Bearer ${await this.getAccessToken(forceToken)}`;
|
|
228
|
-
} else if (/^cx3wk_[a-f0-9]{48}$/i.test(this.#apiKey)) {
|
|
229
|
-
headers["X-Api-Key"] = this.#apiKey;
|
|
230
|
-
}
|
|
210
|
+
if (this.oauth) headers.Authorization = `Bearer ${await this.getAccessToken(forceToken)}`;
|
|
231
211
|
return headers;
|
|
232
212
|
}
|
|
233
213
|
|
|
@@ -252,9 +232,6 @@ class Cx3Provider {
|
|
|
252
232
|
* @param {{ method?: string, body?: object, query?: Record<string,string|number> }} [opts]
|
|
253
233
|
*/
|
|
254
234
|
async request(path, opts = {}) {
|
|
255
|
-
if (!this.oauth && this.apiKeyMode === "invalid") {
|
|
256
|
-
throw new Cx3Error("api_key_required", "api_key_required");
|
|
257
|
-
}
|
|
258
235
|
const method = opts.method || "GET";
|
|
259
236
|
let route = path.startsWith("/") ? path : `/${path}`;
|
|
260
237
|
if (this.gateway) route = gatewayPath(route, opts.body);
|
|
@@ -564,34 +541,14 @@ class Cx3Provider {
|
|
|
564
541
|
}
|
|
565
542
|
|
|
566
543
|
/**
|
|
567
|
-
* Check that this wallet is the one the
|
|
568
|
-
*
|
|
569
|
-
* OAuth2 app and cx3wk_ key → GET `/wallet/api-key/bind` (the gateway route; it has no `/whoami`);
|
|
570
|
-
* a cx3wk_ key falls back to `/whoami` on 404 (a same-origin proxy that only forwards that).
|
|
571
|
-
* A development node has no credentials: `{ ok: true, devNode: true }`.
|
|
544
|
+
* Check that this wallet is the one the application belongs to: `{ ok: true }`, or `wallet_mismatch`.
|
|
545
|
+
* A development node has no application: `{ ok: true, devNode: true }`.
|
|
572
546
|
*/
|
|
573
547
|
async bindApiKey(address) {
|
|
574
548
|
const addr = normalizeAddress(address);
|
|
575
549
|
if (!addr) throw new Cx3Error("address required", "NO_ADDRESS");
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const mode = this.apiKeyMode;
|
|
579
|
-
if (mode === "invalid") {
|
|
580
|
-
throw new Cx3Error("api_key_required", "api_key_required");
|
|
581
|
-
}
|
|
582
|
-
if (mode === "public") {
|
|
583
|
-
return { ok: true, devNode: true };
|
|
584
|
-
}
|
|
585
|
-
try {
|
|
586
|
-
const json = await this.request("/wallet/api-key/bind", { query });
|
|
587
|
-
return this._rejectEnvelope(json);
|
|
588
|
-
} catch (e) {
|
|
589
|
-
if (e && e.status === 404) {
|
|
590
|
-
const json = await this.request("/whoami", { query });
|
|
591
|
-
return this._rejectEnvelope(json);
|
|
592
|
-
}
|
|
593
|
-
throw e;
|
|
594
|
-
}
|
|
550
|
+
if (!this.oauth) return { ok: true, devNode: true };
|
|
551
|
+
return this._rejectEnvelope(await this.request("/wallet/api-key/bind", { query: { address: addr } }));
|
|
595
552
|
}
|
|
596
553
|
|
|
597
554
|
/**
|
package/src/utils.js
CHANGED
|
@@ -70,17 +70,6 @@ function toJsonSafe(value) {
|
|
|
70
70
|
return value;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
74
|
-
* Same rules as DappsExamples/CX3AssetDashboard `cx3_dapp_key_mode`:
|
|
75
|
-
* empty → public (any wallet); valid cx3wk_ → private (that wallet); else invalid.
|
|
76
|
-
*/
|
|
77
|
-
function walletApiKeyMode(key) {
|
|
78
|
-
const n = String(key == null ? "" : key).trim();
|
|
79
|
-
if (!n) return "public";
|
|
80
|
-
if (/^cx3wk_[a-f0-9]{48}$/i.test(n)) return "private";
|
|
81
|
-
return "invalid";
|
|
82
|
-
}
|
|
83
|
-
|
|
84
73
|
module.exports = {
|
|
85
74
|
readCallValue,
|
|
86
75
|
strip0x,
|
|
@@ -88,6 +77,5 @@ module.exports = {
|
|
|
88
77
|
normalizeAddress,
|
|
89
78
|
sleep,
|
|
90
79
|
isPlainObject,
|
|
91
|
-
toJsonSafe
|
|
92
|
-
walletApiKeyMode
|
|
80
|
+
toJsonSafe
|
|
93
81
|
};
|
package/src/wallet.js
CHANGED
|
@@ -9,12 +9,14 @@ const {
|
|
|
9
9
|
privateKeyToPublicKey,
|
|
10
10
|
signMessage,
|
|
11
11
|
signPersonalMessage,
|
|
12
|
+
signWithSigner,
|
|
13
|
+
publicKeyForms,
|
|
12
14
|
createPrivateKey,
|
|
13
15
|
sha256
|
|
14
16
|
} = require("./crypto");
|
|
15
17
|
const { normalizeAddress, toJsonSafe } = require("./utils");
|
|
16
18
|
const { Cx3Error } = require("./errors");
|
|
17
|
-
const { buildDeployDigest, buildSendDigest, buildOwnerActionDigest, buildTransferPayload, stripNativeAddress, deployContentText } = require("./digest");
|
|
19
|
+
const { buildDeployDigest, buildSendDigest, buildOwnerActionDigest, buildTransferPayload, stripNativeAddress, deployContentText, personalMessageBytes } = require("./digest");
|
|
18
20
|
const { TransactionResponse } = require("./transaction");
|
|
19
21
|
|
|
20
22
|
/** Green Chain deploy rule: a token symbol is capital letters A-Z and digits 0-9 only. */
|
|
@@ -84,36 +86,69 @@ function withDeployWait(result, provider) {
|
|
|
84
86
|
return result;
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
/** A key that lives outside this process (Cx3Wallet.fromSigner): its public key and its sign function. */
|
|
90
|
+
class ExternalSigner {
|
|
91
|
+
constructor(spec) {
|
|
92
|
+
if (!spec || typeof spec.sign !== "function") throw new Cx3Error("signer.sign must be a function", "INVALID_SIGNER");
|
|
93
|
+
const forms = publicKeyForms(spec.publicKey);
|
|
94
|
+
this.compressed = forms.compressed;
|
|
95
|
+
this.uncompressed = forms.uncompressed;
|
|
96
|
+
this.sign = (hash, info) => spec.sign(hash, info);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
87
100
|
/**
|
|
88
|
-
*
|
|
101
|
+
* secp256k1 wallet for Green Chain digests: a private key in this process, or a signer that keeps the
|
|
102
|
+
* key elsewhere (Cx3Wallet.fromSigner).
|
|
89
103
|
*/
|
|
90
104
|
class Cx3Wallet {
|
|
91
105
|
// The key is a private field: JSON.stringify, util.inspect, console.log, Object.keys and a spread of
|
|
92
106
|
// the wallet (or of a result that holds it) never show it. It leaves only through exportPrivateKey().
|
|
93
|
-
#privateKey;
|
|
107
|
+
#privateKey = null;
|
|
108
|
+
#signer = null;
|
|
94
109
|
|
|
95
110
|
/**
|
|
96
111
|
* @param {string|Uint8Array|Buffer} privateKey
|
|
97
112
|
* @param {import('./provider').Cx3Provider|null} [provider]
|
|
98
113
|
*/
|
|
99
114
|
constructor(privateKey, provider = null) {
|
|
115
|
+
this.provider = provider;
|
|
116
|
+
if (privateKey instanceof ExternalSigner) {
|
|
117
|
+
this.#signer = privateKey;
|
|
118
|
+
this.address = normalizeAddress("0x" + sha256(privateKey.uncompressed).slice(-20).toString("hex"));
|
|
119
|
+
this.publicKey = privateKey.compressed.toString("hex");
|
|
120
|
+
this.external = true;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
100
123
|
if (!privateKey) throw new Cx3Error("privateKey required", "NO_PRIVATE_KEY");
|
|
101
124
|
this.#privateKey = privateKey;
|
|
102
|
-
this.provider = provider;
|
|
103
125
|
this.address = privateKeyToAddress(privateKey);
|
|
104
126
|
this.publicKey = privateKeyToPublicKey(privateKey, true).toString("hex");
|
|
127
|
+
this.external = false;
|
|
105
128
|
}
|
|
106
129
|
|
|
107
130
|
static createRandom(provider = null) {
|
|
108
131
|
return new Cx3Wallet(createPrivateKey(), provider);
|
|
109
132
|
}
|
|
110
133
|
|
|
134
|
+
/**
|
|
135
|
+
* A wallet whose key never enters this process (an HSM, a KMS, a hardware wallet, a custody service).
|
|
136
|
+
* `signer.publicKey`: the key's public key, compressed or uncompressed (hex or bytes).
|
|
137
|
+
* `signer.sign(hash, info)`: gets the 32-byte sha256 to sign and `info` ({ kind, payload }: what the
|
|
138
|
+
* hash is of, to show or to check a policy against) and returns r || s (64 bytes, hex or bytes) or
|
|
139
|
+
* { r, s }. The SDK checks the answer against the public key (SIGNER_BAD_SIGNATURE) before using it.
|
|
140
|
+
*/
|
|
141
|
+
static fromSigner(signer, provider = null) {
|
|
142
|
+
return new Cx3Wallet(new ExternalSigner(signer), provider);
|
|
143
|
+
}
|
|
144
|
+
|
|
111
145
|
connect(provider) {
|
|
112
|
-
return new Cx3Wallet(this.#privateKey, provider);
|
|
146
|
+
return new Cx3Wallet(this.#signer || this.#privateKey, provider);
|
|
113
147
|
}
|
|
114
148
|
|
|
115
149
|
/** The private key as given to the constructor (a createRandom() wallet: the only way to save it). */
|
|
116
150
|
exportPrivateKey() {
|
|
151
|
+
if (this.#signer) throw new Cx3Error("This wallet signs with an external signer and holds no private key", "NO_PRIVATE_KEY");
|
|
117
152
|
return this.#privateKey;
|
|
118
153
|
}
|
|
119
154
|
|
|
@@ -141,16 +176,19 @@ class Cx3Wallet {
|
|
|
141
176
|
if (isChainPayload(message)) {
|
|
142
177
|
throw new Cx3Error("This text is a chain transaction or call, not a message", "SIGN_REFUSED_TRANSACTION_PAYLOAD");
|
|
143
178
|
}
|
|
179
|
+
if (this.#signer) return signWithSigner(message, this.#signer, "message");
|
|
144
180
|
return signMessage(message, this.#privateKey);
|
|
145
181
|
}
|
|
146
182
|
|
|
147
183
|
/** The wallet's own typed digests and transfer payloads. */
|
|
148
184
|
#signPayload(payload) {
|
|
185
|
+
if (this.#signer) return signWithSigner(payload, this.#signer, "transaction");
|
|
149
186
|
return signMessage(payload, this.#privateKey);
|
|
150
187
|
}
|
|
151
188
|
|
|
152
189
|
/** Sign a site message the way the CX3Wallet extension and app do (with the site-message prefix). */
|
|
153
190
|
async signPersonalMessage(message) {
|
|
191
|
+
if (this.#signer) return signWithSigner(personalMessageBytes(message), this.#signer, "personalMessage");
|
|
154
192
|
return signPersonalMessage(message, this.#privateKey);
|
|
155
193
|
}
|
|
156
194
|
|
|
@@ -475,6 +513,7 @@ class Cx3Wallet {
|
|
|
475
513
|
return new TransactionResponse(result, this.provider, opts);
|
|
476
514
|
}
|
|
477
515
|
|
|
516
|
+
/** Is this wallet the one the provider's application belongs to: `{ ok: true }`, or `wallet_mismatch`. */
|
|
478
517
|
async bindApiKey(address) {
|
|
479
518
|
if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
|
|
480
519
|
return this.provider.bindApiKey(address || this.address);
|