@carbonx3/cx3.js 0.3.10 → 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 +48 -39
- package/index.d.ts +38 -18
- package/index.js +3 -3
- package/package.json +3 -3
- package/src/crypto.js +88 -0
- package/src/provider.js +53 -60
- package/src/utils.js +1 -13
- package/src/wallet.js +70 -19
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,13 +328,14 @@ 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) |
|
|
314
335
|
| `RATE_LIMITED` | The application's rate limit was reached (429) |
|
|
315
336
|
| `INSUFFICIENT_BALANCE`, `GAS_PRICE_TOO_LOW` | A native transfer's sender cannot pay the amount plus gas, or the gas price is below the chain floor |
|
|
316
|
-
| `INVALID_AMOUNT`, `INVALID_AMOUNT_PRECISION` | A native amount is not greater than 0, or has more than 6 decimal places (CX3 has 6); `transferNative` checks
|
|
337
|
+
| `INVALID_AMOUNT`, `INVALID_AMOUNT_PRECISION` | A native amount is not greater than 0, is a text that is not plain decimal digits (`"0x10"`, `"1e3"`), or has more than 6 decimal places (CX3 has 6); `transferNative` checks before signing |
|
|
338
|
+
| `INVALID_RECIPIENT` | `transferNative` was given a recipient that is not a full address (`0x` or `cx3:` and 40 hex digits); nothing is signed |
|
|
317
339
|
| `GAS_LIMIT_TOO_LOW`, `GAS_LIMIT_TOO_HIGH` | A native transfer's gas limit is outside 21,000–100,000 |
|
|
318
340
|
| `GAS_LIMIT_EXCEEDED` | A contract send's `gasLimit` is above the chain's maximum (400, `max` in the body) |
|
|
319
341
|
| `INVALID_NONCE_ORDER` | A native transfer's nonce is not the next one (`expectedNonce` in the answer) |
|
|
@@ -331,44 +353,31 @@ try {
|
|
|
331
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 |
|
|
332
354
|
| `REQUEST_LOOKUP_UNAVAILABLE` | `getRequest` could not ask the chain (503): try again later, do not sign again |
|
|
333
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 |
|
|
334
|
-
| `
|
|
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 |
|
|
335
359
|
| `NO_CHAIN_IDENTITY` | The network's chain id or genesis hash is unavailable; a native transfer is not signed without them |
|
|
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`) |
|
|
361
|
+
| `INSECURE_BASE_URL` | Credentials would travel over plain `http` to another machine; use `https` (or `allowInsecure: true` on a network you trust) |
|
|
336
362
|
|
|
337
363
|
## Browser applications
|
|
338
364
|
|
|
339
|
-
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.
|
|
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.
|
|
340
366
|
|
|
341
367
|
## Environment variables
|
|
342
368
|
|
|
343
369
|
| Variable | Used for |
|
|
344
370
|
|----------|----------|
|
|
345
|
-
| `CX3_CLIENT_ID`, `CX3_CLIENT_SECRET` | Application credentials when the options are omitted |
|
|
346
|
-
| `CX3_API_KEY` | Legacy wallet API key |
|
|
371
|
+
| `CX3_CLIENT_ID`, `CX3_CLIENT_SECRET` | Application credentials when the options are omitted (not read with `devNode: true`) |
|
|
347
372
|
| `CX3_NODE_URL` | Gateway URL for `getDefaultProvider()` |
|
|
348
373
|
|
|
349
374
|
## TypeScript
|
|
350
375
|
|
|
351
376
|
Type declarations (`index.d.ts`) ship with the package.
|
|
352
377
|
|
|
353
|
-
##
|
|
354
|
-
|
|
355
|
-
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.
|
|
356
|
-
|
|
357
|
-
- **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.
|
|
358
|
-
- **No fallback chain id.** `buildTransferPayload` throws `NO_CHAIN_IDENTITY` without `chainId` and `genesisHash`; the `CX3_CHAIN_ID` variable and the `chainIdBind` option are gone.
|
|
359
|
-
- **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.
|
|
360
|
-
- **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.
|
|
361
|
-
- **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)`.
|
|
362
|
-
|
|
363
|
-
## Upgrading from 0.1.x
|
|
364
|
-
|
|
365
|
-
CX3js is in beta (0.x), so a minor version may contain breaking changes. Version 0.2.0:
|
|
378
|
+
## Versioning
|
|
366
379
|
|
|
367
|
-
|
|
368
|
-
- **HTTP Basic was removed.** The `auth` option throws `BASIC_AUTH_UNSUPPORTED`, and `headers` cannot carry `Authorization` or `X-Api-Key`.
|
|
369
|
-
- **Node.js 20.19 or later** is required (`@noble/secp256k1` 3.x).
|
|
370
|
-
- **Fixes:** BigInt params are signed and sent as decimal strings, and `transferNative` sends the gas fields it signs.
|
|
371
|
-
- **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.
|
|
372
381
|
|
|
373
382
|
## Documentation
|
|
374
383
|
|
package/index.d.ts
CHANGED
|
@@ -29,15 +29,16 @@ 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
|
-
/**
|
|
33
|
-
apiKey?: string;
|
|
34
|
-
/** a development node that runs without auth (/api/v1), no credentials */
|
|
32
|
+
/** a development node that runs without auth (/api/v1); the credentials of the environment are not read for it */
|
|
35
33
|
devNode?: boolean;
|
|
34
|
+
/** The network this provider is for (both together): an endpoint on another network is refused with CHAIN_IDENTITY_MISMATCH and nothing is signed */
|
|
35
|
+
chainId?: string;
|
|
36
|
+
genesisHash?: string;
|
|
37
|
+
/** Credentials over plain http to another machine throw INSECURE_BASE_URL unless this is true (localhost is always allowed) */
|
|
38
|
+
allowInsecure?: boolean;
|
|
36
39
|
/** "" on the gateway, "/api/v1" on a development node */
|
|
37
40
|
apiPrefix?: string;
|
|
38
|
-
/**
|
|
39
|
-
auth?: never;
|
|
40
|
-
/** 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) */
|
|
41
42
|
headers?: Record<string, string>;
|
|
42
43
|
fetch?: typeof fetch;
|
|
43
44
|
timeoutMs?: number;
|
|
@@ -73,14 +74,13 @@ export class Cx3Provider {
|
|
|
73
74
|
devNode: boolean;
|
|
74
75
|
oauth: boolean;
|
|
75
76
|
clientId: string;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
/** Throws CREDENTIALS_REQUIRED without clientId / clientSecret, apiKey or devNode */
|
|
77
|
+
authMode: "oauth" | "devNode";
|
|
78
|
+
/** Throws CREDENTIALS_REQUIRED without clientId / clientSecret or devNode */
|
|
79
79
|
constructor(baseUrl: string, options?: Cx3ProviderOptions);
|
|
80
80
|
/** Forget the held access token: the next request asks for a new one */
|
|
81
81
|
clearAccessToken(): void;
|
|
82
82
|
/** Where the provider points and how it signs in; never a credential (JSON.stringify and util.inspect use it) */
|
|
83
|
-
toJSON(): { baseUrl: string; authMode: "oauth" | "
|
|
83
|
+
toJSON(): { baseUrl: string; authMode: "oauth" | "devNode"; clientId?: string; scope?: string };
|
|
84
84
|
/** The current OAuth2 access token (renewed a minute before it expires); null without an OAuth2 app */
|
|
85
85
|
getAccessToken(force?: boolean): Promise<string | null>;
|
|
86
86
|
request(path: string, opts?: { method?: string; body?: unknown; query?: Record<string, string | number> }): Promise<any>;
|
|
@@ -112,6 +112,7 @@ export class Cx3Provider {
|
|
|
112
112
|
getBalance(address: string): Promise<any>;
|
|
113
113
|
getNonce(address: string): Promise<number>;
|
|
114
114
|
getPendingNonce(address: string): Promise<any>;
|
|
115
|
+
/** Is this wallet the one the application belongs to: { ok: true }, or wallet_mismatch */
|
|
115
116
|
bindApiKey(address: string): Promise<any>;
|
|
116
117
|
/** The gateway lists a private token only when the app's wallet is its owner or an authorized wallet */
|
|
117
118
|
listContracts(opts?: { limit?: number; offset?: number; viewer?: string; owner?: string; standard?: string; type?: string; live?: boolean }): Promise<any>;
|
|
@@ -131,14 +132,32 @@ export class Cx3Provider {
|
|
|
131
132
|
getContract(address: string, abi?: unknown, signer?: Cx3Wallet | null): Promise<Contract>;
|
|
132
133
|
}
|
|
133
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
|
+
|
|
134
149
|
export class Cx3Wallet {
|
|
135
150
|
address: string;
|
|
136
151
|
publicKey: string;
|
|
137
152
|
provider: Cx3Provider | null;
|
|
153
|
+
/** true for a wallet made with fromSigner (no private key in this process) */
|
|
154
|
+
external: boolean;
|
|
138
155
|
constructor(privateKey: string | Uint8Array | Buffer, provider?: Cx3Provider | null);
|
|
139
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;
|
|
140
159
|
connect(provider: Cx3Provider): Cx3Wallet;
|
|
141
|
-
/** 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 */
|
|
142
161
|
exportPrivateKey(): string | Uint8Array | Buffer;
|
|
143
162
|
/** Address and public key only (JSON.stringify and util.inspect use it) */
|
|
144
163
|
toJSON(): { address: string; publicKey: string };
|
|
@@ -165,15 +184,15 @@ export class Cx3Wallet {
|
|
|
165
184
|
signSend(contractAddress: string, method: string, params?: object, opts?: Cx3ChainIdentity): Promise<any>;
|
|
166
185
|
signOwnerAction(action: string, address: string, owner?: string | null, opts?: Cx3ChainIdentity): Promise<any>;
|
|
167
186
|
/** A private token's transfer / transferFrom / mint is checked first (PRIVATE_TOKEN_SENDER / _RECIPIENT); `privateCheck: false` skips it */
|
|
168
|
-
sendTransaction(contractAddress: string, method: string, params?: object, opts?: { wait?: number; async?: boolean; privateCheck?: boolean; [k: string]: unknown }): Promise<TransactionResponse>;
|
|
187
|
+
sendTransaction(contractAddress: string, method: string, params?: object, opts?: { wait?: number; async?: boolean; privateCheck?: boolean; chainId?: string; genesisHash?: string; [k: string]: unknown }): Promise<TransactionResponse>;
|
|
169
188
|
/** 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. A dropped deploy is pending: result.wait() follows it. */
|
|
170
189
|
/** Sends an earlier send's signed request again, unchanged (no new signature): after SEND_OUTCOME_UNKNOWN or a lost answer */
|
|
171
190
|
resendTransaction(signedRequest: Cx3SignedRequest, opts?: { wait?: number; async?: boolean }): Promise<TransactionResponse>;
|
|
172
|
-
deployContract(fields: object): Promise<Cx3DeployResult>;
|
|
173
|
-
pauseContract(address: string, owner?: string | null): Promise<any>;
|
|
174
|
-
unpauseContract(address: string, owner?: string | null): Promise<any>;
|
|
175
|
-
deleteContract(address: string, owner?: string | null): Promise<any>;
|
|
176
|
-
/**
|
|
191
|
+
deployContract(fields: object, opts?: Cx3ChainIdentity): Promise<Cx3DeployResult>;
|
|
192
|
+
pauseContract(address: string, owner?: string | null, opts?: Cx3ChainIdentity): Promise<any>;
|
|
193
|
+
unpauseContract(address: string, owner?: string | null, opts?: Cx3ChainIdentity): Promise<any>;
|
|
194
|
+
deleteContract(address: string, owner?: string | null, opts?: Cx3ChainIdentity): Promise<any>;
|
|
195
|
+
/** Before signing, throws Cx3Error INVALID_RECIPIENT (not 0x / cx3: + 40 hex), INVALID_AMOUNT (not greater than 0, or a text that is not plain decimal digits) or INVALID_AMOUNT_PRECISION (more than 6 decimals) */
|
|
177
196
|
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>;
|
|
178
197
|
bindApiKey(address?: string): Promise<any>;
|
|
179
198
|
}
|
|
@@ -290,7 +309,6 @@ export class TransactionResponse {
|
|
|
290
309
|
export function getDefaultProvider(options?: Cx3ProviderOptions & { url?: string }): Cx3Provider;
|
|
291
310
|
export function readCallValue(response: any): unknown;
|
|
292
311
|
export function normalizeAddress(addr: string): string;
|
|
293
|
-
export function walletApiKeyMode(key?: string | null): "public" | "private" | "invalid";
|
|
294
312
|
/** BigInt → decimal string, recursively (params are signed and sent in this form) */
|
|
295
313
|
export function toJsonSafe<T>(value: T): unknown;
|
|
296
314
|
export function ensure0x(hex: string): string;
|
|
@@ -312,6 +330,8 @@ export function signPersonalMessage(message: string, privateKey: string | Uint8A
|
|
|
312
330
|
export function buildTransferPayload(fields: object): string;
|
|
313
331
|
export function signMessage(message: string | Uint8Array, privateKey: string | Uint8Array | Buffer): Promise<any>;
|
|
314
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;
|
|
315
335
|
export function createPrivateKey(): string;
|
|
316
336
|
export function normalizeAbi(abi: unknown): Record<string, any>;
|
|
317
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"
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=20.19.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@noble/secp256k1": "
|
|
43
|
+
"@noble/secp256k1": "3.2.0"
|
|
44
44
|
},
|
|
45
45
|
"exports": {
|
|
46
46
|
".": {
|
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 || {})) {
|
|
@@ -65,7 +64,16 @@ function withoutCredentialHeaders(headers) {
|
|
|
65
64
|
return out;
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
|
|
67
|
+
// http to another machine (not localhost / 127.x / ::1)
|
|
68
|
+
function isRemotePlainHttp(baseUrl) {
|
|
69
|
+
let u;
|
|
70
|
+
try { u = new URL(baseUrl); } catch (_) { return false; }
|
|
71
|
+
if (u.protocol !== "http:") return false;
|
|
72
|
+
const host = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
73
|
+
return !(host === "localhost" || host.endsWith(".localhost") || host === "::1" || /^127(\.\d{1,3}){3}$/.test(host));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const CLIENT_ID_RE =/^cx3app_[a-f0-9]{24}$/i;
|
|
69
77
|
const CLIENT_SECRET_RE = /^cx3sec_[a-f0-9]{64}$/i;
|
|
70
78
|
|
|
71
79
|
// a dropped tx is failed for good after its finalAfter: wait this much past it (clock skew)
|
|
@@ -81,7 +89,6 @@ class Cx3Provider {
|
|
|
81
89
|
// Credentials live in private fields: JSON.stringify, util.inspect, console.log, Object.keys and a
|
|
82
90
|
// spread of the provider (or of anything that holds it) never show them.
|
|
83
91
|
#clientSecret = "";
|
|
84
|
-
#apiKey = "";
|
|
85
92
|
#token = null; // { value, refreshAt, scope }
|
|
86
93
|
|
|
87
94
|
/**
|
|
@@ -90,7 +97,6 @@ class Cx3Provider {
|
|
|
90
97
|
* @param {string} [options.clientId] OAuth2 client id (`cx3app_…`; env CX3_CLIENT_ID)
|
|
91
98
|
* @param {string} [options.clientSecret] OAuth2 client secret (`cx3sec_…`; env CX3_CLIENT_SECRET)
|
|
92
99
|
* @param {string|string[]} [options.scope] ask for fewer scopes than the app has ("read", "write", "deploy")
|
|
93
|
-
* @param {string} [options.apiKey] wallet API key (`cx3wk_…`; env CX3_API_KEY), until apps move to OAuth2
|
|
94
100
|
* @param {boolean} [options.devNode] a development node without auth (`/api/v1`), no credentials
|
|
95
101
|
* @param {string} [options.apiPrefix] ("" on the gateway, "/api/v1" on a development node)
|
|
96
102
|
* @param {Record<string,string>} [options.headers] Extra headers (Authorization / X-Api-Key are dropped)
|
|
@@ -100,29 +106,40 @@ class Cx3Provider {
|
|
|
100
106
|
constructor(baseUrl, options = {}) {
|
|
101
107
|
if (!baseUrl) throw new Cx3Error("Cx3Provider requires baseUrl (CX3_NODE_URL)", "NO_BASE_URL");
|
|
102
108
|
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"BASIC_AUTH_UNSUPPORTED"
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
const env = (name) => (process.env[name] || "").trim();
|
|
109
|
+
this.devNode = options.devNode === true;
|
|
110
|
+
// a development node gets only what the caller passes: the gateway credentials of the environment never go to it
|
|
111
|
+
const env = (name) => (this.devNode ? "" : (process.env[name] || "").trim());
|
|
110
112
|
this.clientId = String(options.clientId != null ? options.clientId : env("CX3_CLIENT_ID")).trim();
|
|
111
113
|
this.#clientSecret = String(options.clientSecret != null ? options.clientSecret : env("CX3_CLIENT_SECRET")).trim();
|
|
112
114
|
this.scope = Array.isArray(options.scope) ? options.scope.join(" ") : String(options.scope || "").trim();
|
|
113
|
-
this.#apiKey = String(options.apiKey != null ? options.apiKey : env("CX3_API_KEY")).trim();
|
|
114
|
-
this.devNode = options.devNode === true;
|
|
115
115
|
this.oauth = !!(this.clientId || this.#clientSecret);
|
|
116
116
|
if (this.oauth && (!CLIENT_ID_RE.test(this.clientId) || !CLIENT_SECRET_RE.test(this.#clientSecret))) {
|
|
117
117
|
throw new Cx3Error("clientId must be cx3app_… and clientSecret cx3sec_… (CX3WebUI → Settings → API → Applications)", "INVALID_CLIENT_CREDENTIALS");
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
if (!this.oauth && !hasKey && !this.devNode) {
|
|
119
|
+
if (!this.oauth && !this.devNode) {
|
|
121
120
|
throw new Cx3Error(
|
|
122
121
|
"App credentials required: create an application in CX3WebUI (Settings → API → Applications) and pass clientId / clientSecret (or set CX3_CLIENT_ID / CX3_CLIENT_SECRET)",
|
|
123
122
|
"CREDENTIALS_REQUIRED"
|
|
124
123
|
);
|
|
125
124
|
}
|
|
125
|
+
// credentials over plain http can be read on the way: only this machine, or the caller's own word
|
|
126
|
+
if (this.oauth && options.allowInsecure !== true && isRemotePlainHttp(this.baseUrl)) {
|
|
127
|
+
throw new Cx3Error(
|
|
128
|
+
"Credentials are not sent over plain http: use an https URL (or allowInsecure: true on a network you trust)",
|
|
129
|
+
"INSECURE_BASE_URL"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
// The network this provider is for: with both given, signatures are made for this network only and
|
|
133
|
+
// an endpoint that names another one is refused (CHAIN_IDENTITY_MISMATCH).
|
|
134
|
+
this.pinnedIdentity = null;
|
|
135
|
+
if (options.chainId != null || options.genesisHash != null) {
|
|
136
|
+
const chainId = String(options.chainId == null ? "" : options.chainId).trim();
|
|
137
|
+
const genesisHash = normalizeGenesisHash(options.genesisHash);
|
|
138
|
+
if (!chainId || !genesisHash) {
|
|
139
|
+
throw new Cx3Error("chainId and genesisHash are given together (genesisHash: 64 hex)", "NO_CHAIN_IDENTITY");
|
|
140
|
+
}
|
|
141
|
+
this.pinnedIdentity = { chainId, genesisHash };
|
|
142
|
+
}
|
|
126
143
|
this.gateway = !this.devNode;
|
|
127
144
|
this.apiPrefix = options.apiPrefix != null ? options.apiPrefix : (this.gateway ? "" : "/api/v1");
|
|
128
145
|
this.extraHeaders = withoutCredentialHeaders(options.headers);
|
|
@@ -136,14 +153,9 @@ class Cx3Provider {
|
|
|
136
153
|
}
|
|
137
154
|
}
|
|
138
155
|
|
|
139
|
-
|
|
140
|
-
return walletApiKeyMode(this.#apiKey);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** "oauth" | "apiKey" | "devNode" */
|
|
156
|
+
/** "oauth" | "devNode" */
|
|
144
157
|
get authMode() {
|
|
145
|
-
|
|
146
|
-
return this.oauth ? "oauth" : "apiKey";
|
|
158
|
+
return this.oauth ? "oauth" : "devNode";
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
/**
|
|
@@ -195,11 +207,7 @@ class Cx3Provider {
|
|
|
195
207
|
"Content-Type": "application/json",
|
|
196
208
|
...this.extraHeaders
|
|
197
209
|
};
|
|
198
|
-
if (this.oauth) {
|
|
199
|
-
headers.Authorization = `Bearer ${await this.getAccessToken(forceToken)}`;
|
|
200
|
-
} else if (/^cx3wk_[a-f0-9]{48}$/i.test(this.#apiKey)) {
|
|
201
|
-
headers["X-Api-Key"] = this.#apiKey;
|
|
202
|
-
}
|
|
210
|
+
if (this.oauth) headers.Authorization = `Bearer ${await this.getAccessToken(forceToken)}`;
|
|
203
211
|
return headers;
|
|
204
212
|
}
|
|
205
213
|
|
|
@@ -224,9 +232,6 @@ class Cx3Provider {
|
|
|
224
232
|
* @param {{ method?: string, body?: object, query?: Record<string,string|number> }} [opts]
|
|
225
233
|
*/
|
|
226
234
|
async request(path, opts = {}) {
|
|
227
|
-
if (!this.oauth && this.apiKeyMode === "invalid") {
|
|
228
|
-
throw new Cx3Error("api_key_required", "api_key_required");
|
|
229
|
-
}
|
|
230
235
|
const method = opts.method || "GET";
|
|
231
236
|
let route = path.startsWith("/") ? path : `/${path}`;
|
|
232
237
|
if (this.gateway) route = gatewayPath(route, opts.body);
|
|
@@ -432,6 +437,14 @@ class Cx3Provider {
|
|
|
432
437
|
const chainId = typeof r.chainId === "string" ? r.chainId.trim() : "";
|
|
433
438
|
const genesisHash = normalizeGenesisHash(r.genesisHash);
|
|
434
439
|
if (!chainId || !genesisHash) throw new Cx3Error("network identity unavailable", "NO_CHAIN_IDENTITY");
|
|
440
|
+
const pin = this.pinnedIdentity;
|
|
441
|
+
if (pin && (pin.chainId !== chainId || pin.genesisHash !== genesisHash)) {
|
|
442
|
+
throw new Cx3Error(
|
|
443
|
+
"The endpoint is on another network than the one this provider was made for: nothing is signed",
|
|
444
|
+
"CHAIN_IDENTITY_MISMATCH",
|
|
445
|
+
{ expected: { ...pin }, got: { chainId, genesisHash } }
|
|
446
|
+
);
|
|
447
|
+
}
|
|
435
448
|
this._chainIdentity = { at: Date.now(), value: { chainId, genesisHash } };
|
|
436
449
|
return { chainId, genesisHash };
|
|
437
450
|
}
|
|
@@ -528,34 +541,14 @@ class Cx3Provider {
|
|
|
528
541
|
}
|
|
529
542
|
|
|
530
543
|
/**
|
|
531
|
-
* Check that this wallet is the one the
|
|
532
|
-
*
|
|
533
|
-
* OAuth2 app and cx3wk_ key → GET `/wallet/api-key/bind` (the gateway route; it has no `/whoami`);
|
|
534
|
-
* a cx3wk_ key falls back to `/whoami` on 404 (a same-origin proxy that only forwards that).
|
|
535
|
-
* 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 }`.
|
|
536
546
|
*/
|
|
537
547
|
async bindApiKey(address) {
|
|
538
548
|
const addr = normalizeAddress(address);
|
|
539
549
|
if (!addr) throw new Cx3Error("address required", "NO_ADDRESS");
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const mode = this.apiKeyMode;
|
|
543
|
-
if (mode === "invalid") {
|
|
544
|
-
throw new Cx3Error("api_key_required", "api_key_required");
|
|
545
|
-
}
|
|
546
|
-
if (mode === "public") {
|
|
547
|
-
return { ok: true, devNode: true };
|
|
548
|
-
}
|
|
549
|
-
try {
|
|
550
|
-
const json = await this.request("/wallet/api-key/bind", { query });
|
|
551
|
-
return this._rejectEnvelope(json);
|
|
552
|
-
} catch (e) {
|
|
553
|
-
if (e && e.status === 404) {
|
|
554
|
-
const json = await this.request("/whoami", { query });
|
|
555
|
-
return this._rejectEnvelope(json);
|
|
556
|
-
}
|
|
557
|
-
throw e;
|
|
558
|
-
}
|
|
550
|
+
if (!this.oauth) return { ok: true, devNode: true };
|
|
551
|
+
return this._rejectEnvelope(await this.request("/wallet/api-key/bind", { query: { address: addr } }));
|
|
559
552
|
}
|
|
560
553
|
|
|
561
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
|
|
|
@@ -235,7 +273,7 @@ class Cx3Wallet {
|
|
|
235
273
|
if (opts.privateCheck !== false && typeof this.provider.checkPrivateTokenSend === "function") {
|
|
236
274
|
await this.provider.checkPrivateTokenSend(contractAddress, method, params, this.address);
|
|
237
275
|
}
|
|
238
|
-
const signed = await this.signSend(contractAddress, method, params);
|
|
276
|
+
const signed = await this.signSend(contractAddress, method, params, opts);
|
|
239
277
|
// everything the gateway needs to take the same signed call again: no key, safe to store
|
|
240
278
|
const signedRequest = {
|
|
241
279
|
contractAddress,
|
|
@@ -309,7 +347,7 @@ class Cx3Wallet {
|
|
|
309
347
|
* (Cx3HttpError code DUPLICATE_TOKEN_SYMBOL, status 409). A dropped deploy (202, `dropped`,
|
|
310
348
|
* `finalAfter`) is pending, not deployed: `result.wait()` follows it (withDeployWait).
|
|
311
349
|
*/
|
|
312
|
-
async deployContract(fields) {
|
|
350
|
+
async deployContract(fields, opts = {}) {
|
|
313
351
|
if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
|
|
314
352
|
fields = toJsonSafe(fields || {});
|
|
315
353
|
const ctor = fields.constructorArgs && typeof fields.constructorArgs === "object" && !Array.isArray(fields.constructorArgs) ? fields.constructorArgs : {};
|
|
@@ -329,7 +367,7 @@ class Cx3Wallet {
|
|
|
329
367
|
...fields,
|
|
330
368
|
owner: fields.owner || this.address,
|
|
331
369
|
contentHash
|
|
332
|
-
});
|
|
370
|
+
}, opts);
|
|
333
371
|
const body = {
|
|
334
372
|
...fields,
|
|
335
373
|
owner: signed.owner,
|
|
@@ -347,9 +385,9 @@ class Cx3Wallet {
|
|
|
347
385
|
return withDeployWait(result, this.provider);
|
|
348
386
|
}
|
|
349
387
|
|
|
350
|
-
async _ownerActionRequest(action, address, owner, method, path) {
|
|
388
|
+
async _ownerActionRequest(action, address, owner, method, path, opts = {}) {
|
|
351
389
|
if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
|
|
352
|
-
const signed = await this.signOwnerAction(action, address, owner);
|
|
390
|
+
const signed = await this.signOwnerAction(action, address, owner, opts);
|
|
353
391
|
return this.provider.request(path, {
|
|
354
392
|
method,
|
|
355
393
|
body: {
|
|
@@ -361,33 +399,36 @@ class Cx3Wallet {
|
|
|
361
399
|
});
|
|
362
400
|
}
|
|
363
401
|
|
|
364
|
-
async pauseContract(address, owner = null) {
|
|
402
|
+
async pauseContract(address, owner = null, opts = {}) {
|
|
365
403
|
return this._ownerActionRequest(
|
|
366
404
|
"pause",
|
|
367
405
|
address,
|
|
368
406
|
owner,
|
|
369
407
|
"PUT",
|
|
370
|
-
`/contracts/${encodeURIComponent(address)}/pause
|
|
408
|
+
`/contracts/${encodeURIComponent(address)}/pause`,
|
|
409
|
+
opts
|
|
371
410
|
);
|
|
372
411
|
}
|
|
373
412
|
|
|
374
|
-
async unpauseContract(address, owner = null) {
|
|
413
|
+
async unpauseContract(address, owner = null, opts = {}) {
|
|
375
414
|
return this._ownerActionRequest(
|
|
376
415
|
"unpause",
|
|
377
416
|
address,
|
|
378
417
|
owner,
|
|
379
418
|
"PUT",
|
|
380
|
-
`/contracts/${encodeURIComponent(address)}/unpause
|
|
419
|
+
`/contracts/${encodeURIComponent(address)}/unpause`,
|
|
420
|
+
opts
|
|
381
421
|
);
|
|
382
422
|
}
|
|
383
423
|
|
|
384
|
-
async deleteContract(address, owner = null) {
|
|
424
|
+
async deleteContract(address, owner = null, opts = {}) {
|
|
385
425
|
return this._ownerActionRequest(
|
|
386
426
|
"delete",
|
|
387
427
|
address,
|
|
388
428
|
owner,
|
|
389
429
|
"DELETE",
|
|
390
|
-
`/contracts/${encodeURIComponent(address)}
|
|
430
|
+
`/contracts/${encodeURIComponent(address)}`,
|
|
431
|
+
opts
|
|
391
432
|
);
|
|
392
433
|
}
|
|
393
434
|
|
|
@@ -399,9 +440,18 @@ class Cx3Wallet {
|
|
|
399
440
|
*/
|
|
400
441
|
async transferNative(to, amount, opts = {}) {
|
|
401
442
|
if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
443
|
+
if (to == null || String(to).trim() === "") throw new Cx3Error("recipient required", "NO_RECIPIENT");
|
|
444
|
+
// a full address only (0x / cx3: + 40 hex): anything else is not signed
|
|
445
|
+
const toAddress = normalizeAddress(to);
|
|
446
|
+
if (!/^0x[0-9a-f]{40}$/.test(toAddress)) {
|
|
447
|
+
throw new Cx3Error("The recipient is not a CX3 address (0x + 40 hex)", "INVALID_RECIPIENT");
|
|
448
|
+
}
|
|
449
|
+
// a text amount is plain decimal digits: "0x10" or "1e3" would be read as another number
|
|
450
|
+
if (typeof amount === "string" && !/^\d{1,15}(\.\d+)?$/.test(amount.trim())) {
|
|
451
|
+
throw new Cx3Error("Invalid amount", "INVALID_AMOUNT");
|
|
452
|
+
}
|
|
453
|
+
if (typeof amount !== "number" && typeof amount !== "string") throw new Cx3Error("Invalid amount", "INVALID_AMOUNT");
|
|
454
|
+
const amt = typeof amount === "number" ? amount : Number(amount.trim());
|
|
405
455
|
if (!Number.isFinite(amt) || amt <= 0) {
|
|
406
456
|
throw new Cx3Error("Invalid amount", "INVALID_AMOUNT");
|
|
407
457
|
}
|
|
@@ -463,6 +513,7 @@ class Cx3Wallet {
|
|
|
463
513
|
return new TransactionResponse(result, this.provider, opts);
|
|
464
514
|
}
|
|
465
515
|
|
|
516
|
+
/** Is this wallet the one the provider's application belongs to: `{ ok: true }`, or `wallet_mismatch`. */
|
|
466
517
|
async bindApiKey(address) {
|
|
467
518
|
if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
|
|
468
519
|
return this.provider.bindApiKey(address || this.address);
|