@carbonx3/cx3.js 0.3.5 → 0.3.6
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 +35 -7
- package/index.d.ts +50 -15
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/factory.js +36 -16
- package/src/provider.js +62 -10
- package/src/transaction.js +16 -7
- package/src/wallet.js +42 -2
package/README.md
CHANGED
|
@@ -123,6 +123,7 @@ With the `read` scope:
|
|
|
123
123
|
| `call(address, method, params)` | A read-only call; the result is in `value` |
|
|
124
124
|
| `getEvents(address, { type, limit, offset })`, `getTransfers(address, { limit, offset })`, `getHolders(address, { limit, offset })` | Contract history |
|
|
125
125
|
| `getAddressTransactions(address, limit)`, `getTxStatus(txHash)` | Transactions |
|
|
126
|
+
| `getRequest(requestId, from)` | What became of a signed contract send (public; see [Waiting for a transaction](#waiting-for-a-transaction)) |
|
|
126
127
|
|
|
127
128
|
## Contracts
|
|
128
129
|
|
|
@@ -148,9 +149,24 @@ Supported ABI shapes:
|
|
|
148
149
|
Gas is paid in CX3 to the validator that produces the block: gas used × gas price (devnet default 0.0001 CX3 per gas).
|
|
149
150
|
|
|
150
151
|
- A native transfer uses 21,000 gas: **2.1 CX3**. The sender needs the amount plus the fee.
|
|
151
|
-
- A contract send or deploy through the gateway is relayed by the chain's own deploy account: the call still runs as your wallet (the chain checks your signature again), and your wallet pays no gas for it. The call's gas limit (100,000 by default)
|
|
152
|
+
- A contract send or deploy through the gateway is relayed by the chain's own deploy account: the call still runs as your wallet (the chain checks your signature again), and your wallet pays no gas for it. The call's gas limit (100,000 by default) bounds the node's trial run when the call is sent (a call that needs more is refused); in the block the VM runs the call under its own per-call limit.
|
|
152
153
|
|
|
153
|
-
Too little balance for a native transfer fails with `INSUFFICIENT_BALANCE
|
|
154
|
+
Too little balance for a native transfer fails with `INSUFFICIENT_BALANCE`. A native transfer's gas limit and gas price are `0` (the chain's defaults: 21,000 and 0.0001) or 21,000–100,000 and at least the chain floor of 0.000001 (`GAS_LIMIT_TOO_LOW`, `GAS_LIMIT_TOO_HIGH`, `GAS_PRICE_TOO_LOW`), and its amount is greater than 0 with at most 6 decimal places, because CX3 has 6. `transferNative` checks the amount before signing (`INVALID_AMOUNT`, `INVALID_AMOUNT_PRECISION`).
|
|
155
|
+
|
|
156
|
+
### Waiting for a transaction
|
|
157
|
+
|
|
158
|
+
A contract send waits for its block by default (the node waits up to 25 seconds); with `{ wait: 0 }` it returns at once. `tx.wait()` polls the transaction status until it is confirmed or failed for good:
|
|
159
|
+
|
|
160
|
+
- **Pending is not failed.** `tx.status` is `"pending"` for `{ wait: 0 }`, for a queued send (HTTP 202 `queued: true`, code `TX_TIMEOUT`: not in a block within the node's wait) and for a dropped send. `tx.success` stays `true`, and `tx.wait()` follows the transaction. A queued or dropped deploy is `status: "pending"` too: the token factory gives `result.contract` only after `result.wait()` sees it confirmed.
|
|
161
|
+
- **Dropped sends.** A send the node trimmed from its block comes back as HTTP 202 with `tx.dropped === true` and `tx.finalAfter` (ms epoch, about 12 minutes after the signed timestamp). Until then another validator may still include it, so by default `tx.wait()` keeps waiting until `finalAfter` plus 30 seconds. After that the status says `failed`, and `tx.wait()` rejects with `TX_FAILED`.
|
|
162
|
+
- **Only a final failure rejects with `TX_FAILED`.** `TX_TRACK_TIMEOUT` (your own `timeoutMs`, otherwise 60 seconds or until `finalAfter` for a dropped send) means the transaction is still pending: its `receipt` has `status: "pending"`, `dropped` and `finalAfter`. A network error, a 429 or a 5xx answer while polling does not end the wait.
|
|
163
|
+
- **Never sign the same call again while it can still run.** A new signature is a second call. Every send answer carries `tx.requestId`, and `provider.getRequest(tx.requestId, wallet.address)` tells what became of it: `executed`, `pending`, `submitted` or `unknown`.
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
const tx = await token.transfer(to, amount);
|
|
167
|
+
if (tx.dropped) console.log("Not in a block yet; final after", new Date(tx.finalAfter));
|
|
168
|
+
const receipt = await tx.wait(); // confirmed, or throws TX_FAILED / TX_TRACK_TIMEOUT
|
|
169
|
+
```
|
|
154
170
|
|
|
155
171
|
## Deploying tokens
|
|
156
172
|
|
|
@@ -171,6 +187,8 @@ console.log(result.contractAddress); // result.contract is a ready Cont
|
|
|
171
187
|
| `deployCX3192` | Soulbound identity |
|
|
172
188
|
| `deployCX337` … `deployCX341` | Green standards: CBAM proof, sustainability report, supply chain, materiality assessment, green claims |
|
|
173
189
|
|
|
190
|
+
A deploy the node trimmed from its block answers HTTP 202 with `result.dropped === true`, `status: "pending"`, `finalAfter` and, when known, its `contractAddress` (otherwise `null`): it is not deployed yet, so `result.contract` is not set. `await result.wait()` follows it as `tx.wait()` follows a send: once it is confirmed, `status` becomes `"confirmed"` and the factory adds `result.contract` (and `result.authorized` for a private token); after `finalAfter` it rejects with `TX_FAILED`. Do not deploy the same contract again before then. `wallet.deployContract()` results have the same `dropped`, `finalAfter` and `wait()`.
|
|
191
|
+
|
|
174
192
|
The helpers deploy to CX3WASM by default. The chain renders CX30 and CX337–CX341 from the fields; CX31, CX300, CX315 and CX3192 take their CX3Script source as `sourceCode` (otherwise the chain answers `STANDARD_NOT_WASM_DEPLOYABLE`).
|
|
175
193
|
|
|
176
194
|
Token symbols are capital letters `A-Z` and digits `0-9` only. The SDK checks this before signing (`INVALID_TOKEN_SYMBOL`; see `isValidTokenSymbol`), and the chain keeps one contract per symbol (`409 DUPLICATE_TOKEN_SYMBOL`).
|
|
@@ -207,13 +225,13 @@ Digests are UTF-8 strings, hashed with SHA-256 and signed with secp256k1, byte-i
|
|
|
207
225
|
- **Site message:** `0x19` + `"CX3 Signed Message:\n"` + UTF-8 length + message (`signPersonalMessage`), the way the CX3Wallet extension and app sign `cx3_sign_message`
|
|
208
226
|
- **Native transfer:** `JSON.stringify({ type, fromAddress, toAddress, amount, nonce, timestamp, data, gasLimit, gasPrice, chainId, genesisHash })`, with 40-hex addresses and no `0x`; `chainId` and `genesisHash` (`0x` + 64 hex) are the network's, from `provider.getChainIdentity()`
|
|
209
227
|
|
|
210
|
-
BigInt values in params are converted to decimal strings before signing, and exactly those params are sent.
|
|
228
|
+
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)`.
|
|
211
229
|
|
|
212
230
|
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`.
|
|
213
231
|
|
|
214
232
|
## Error handling
|
|
215
233
|
|
|
216
|
-
Every error is a `Cx3Error` with a `code`. `Cx3HttpError` adds the HTTP `status` and the response `body
|
|
234
|
+
Every error is a `Cx3Error` with a `code`. `Cx3HttpError` adds the HTTP `status` and the response `body` (with `requestId` for a contract send); `Cx3TxError` adds the transaction `receipt`. A send or native transfer that the node refuses with HTTP 200 returns `tx.success === false` with the code in `tx.error`, and `tx.wait()` rejects with a `Cx3TxError` of that code.
|
|
217
235
|
|
|
218
236
|
```javascript
|
|
219
237
|
const { Cx3HttpError } = require("@carbonx3/cx3.js");
|
|
@@ -234,15 +252,25 @@ try {
|
|
|
234
252
|
| `insufficient_scope`, `ROUTE_NOT_ALLOWED` | The route needs another scope, or is not open to applications (403) |
|
|
235
253
|
| `RATE_LIMITED` | The application's rate limit was reached (429) |
|
|
236
254
|
| `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 |
|
|
255
|
+
| `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 both before signing |
|
|
256
|
+
| `GAS_LIMIT_TOO_LOW`, `GAS_LIMIT_TOO_HIGH` | A native transfer's gas limit is outside 21,000–100,000 |
|
|
257
|
+
| `GAS_LIMIT_EXCEEDED` | A contract send's `gasLimit` is above the chain's maximum (400, `max` in the body) |
|
|
258
|
+
| `INVALID_NONCE_ORDER` | A native transfer's nonce is not the next one (`expectedNonce` in the answer) |
|
|
259
|
+
| `DATA_TOO_LARGE`, `DATA_TOO_DEEP` | A transaction's `data` is too large or nested too deeply |
|
|
237
260
|
| `INVALID_TOKEN_SYMBOL`, `DUPLICATE_TOKEN_SYMBOL` | The symbol is not `A-Z` / `0-9`, or is already in use (409) |
|
|
238
261
|
| `PRIVATE_TOKEN_SENDER`, `PRIVATE_TOKEN_RECIPIENT`, `PRIVATE_TOKEN` | Private token rules (see above) |
|
|
239
|
-
| `
|
|
262
|
+
| `TX_TIMEOUT` | The node's 25-second wait ended before a block: queued, not final. A send answer with this code is pending and `tx.wait()` follows it; without a tx hash the outcome is open (look up its `requestId`) |
|
|
263
|
+
| `TX_FAILED` | The transaction failed for good (a dropped send only after its `finalAfter`) |
|
|
264
|
+
| `TX_TRACK_TIMEOUT` | `tx.wait()` stopped while the transaction was still pending: not a failure, do not sign it again |
|
|
265
|
+
| `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 |
|
|
266
|
+
| `REQUEST_LOOKUP_UNAVAILABLE` | `getRequest` could not ask the chain (503): try again later, do not sign again |
|
|
267
|
+
| `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 |
|
|
240
268
|
| `BASIC_AUTH_UNSUPPORTED` | The `auth` option was used |
|
|
241
269
|
| `NO_CHAIN_IDENTITY` | The network's chain id or genesis hash is unavailable; a native transfer is not signed without them |
|
|
242
270
|
|
|
243
271
|
## Browser applications
|
|
244
272
|
|
|
245
|
-
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.
|
|
273
|
+
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.8.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, an API key or a private key in browser JavaScript.
|
|
246
274
|
|
|
247
275
|
## Environment variables
|
|
248
276
|
|
|
@@ -258,7 +286,7 @@ Type declarations (`index.d.ts`) ship with the package.
|
|
|
258
286
|
|
|
259
287
|
## Upgrading from 0.2.x
|
|
260
288
|
|
|
261
|
-
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).
|
|
289
|
+
Version 0.3.0 signs native transfers for the network's new signing format (0.3.1 to 0.3.4 change only this README, the file headers and comments, not the code). In 0.3.5, `bindApiKey` with a wallet API key asks the gateway's `/wallet/api-key/bind` (before, it asked `/whoami`, which the gateway refuses, so it always failed). In 0.3.6, `wait()` follows a send or deploy the gateway reports as not final (`dropped`) until its `finalAfter` instead of failing after 60 s, a `TX_TIMEOUT` answer with a hash is pending, `getRequest()` looks up a signed request, and `transferNative` refuses more than 6 decimals.
|
|
262
290
|
|
|
263
291
|
- **The genesis hash is signed.** `transferNative` asks the network for its chain id and genesis hash (`provider.getChainIdentity()`) unless both are given as `opts.chainId` and `opts.genesisHash`. 0.2.x transfers no longer verify on the chain.
|
|
264
292
|
- **No fallback chain id.** `buildTransferPayload` throws `NO_CHAIN_IDENTITY` without `chainId` and `genesisHash`; the `CX3_CHAIN_ID` variable and the `chainIdBind` option are gone.
|
package/index.d.ts
CHANGED
|
@@ -120,7 +120,10 @@ export class Cx3Provider {
|
|
|
120
120
|
/** A wallet's LP token balance in one CarbonFi pool (pair id = 40 hex) */
|
|
121
121
|
getLpBalance(pairId: string, address: string): Promise<{ success: boolean; balance: string; totalSupply: string; lpContractAddress: string | null; onChain: boolean }>;
|
|
122
122
|
getTxStatus(txHash: string): Promise<any>;
|
|
123
|
-
|
|
123
|
+
/** A dropped tx is followed until its finalAfter (+30 s) unless timeoutMs is given; only a final failure rejects (TX_FAILED) */
|
|
124
|
+
waitForTransaction(txHash: string, opts?: { timeoutMs?: number; intervalMs?: number; finalAfter?: number; onPending?: (status: any) => void; onConfirmed?: (status: any) => void }): Promise<any>;
|
|
125
|
+
/** What became of a signed contract send, by its requestId (public /dapp/v1/requests/:requestId?from=); look it up instead of signing again */
|
|
126
|
+
getRequest(requestId: string, from: string): Promise<{ success: boolean; requestId: string; status: "executed" | "pending" | "submitted" | "unknown"; txHash?: string | null; blockHeight?: number; outcome?: string | null; expiresAt?: number; [k: string]: unknown }>;
|
|
124
127
|
getContract(address: string, abi?: unknown, signer?: Cx3Wallet | null): Promise<Contract>;
|
|
125
128
|
}
|
|
126
129
|
|
|
@@ -151,11 +154,12 @@ export class Cx3Wallet {
|
|
|
151
154
|
signOwnerAction(action: string, address: string, owner?: string | null, opts?: Cx3ChainIdentity): Promise<any>;
|
|
152
155
|
/** A private token's transfer / transferFrom / mint is checked first (PRIVATE_TOKEN_SENDER / _RECIPIENT); `privateCheck: false` skips it */
|
|
153
156
|
sendTransaction(contractAddress: string, method: string, params?: object, opts?: { wait?: number; async?: boolean; privateCheck?: boolean; [k: string]: unknown }): Promise<TransactionResponse>;
|
|
154
|
-
/** 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. */
|
|
155
|
-
deployContract(fields: object): Promise<
|
|
157
|
+
/** 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. */
|
|
158
|
+
deployContract(fields: object): Promise<Cx3DeployResult>;
|
|
156
159
|
pauseContract(address: string, owner?: string | null): Promise<any>;
|
|
157
160
|
unpauseContract(address: string, owner?: string | null): Promise<any>;
|
|
158
161
|
deleteContract(address: string, owner?: string | null): Promise<any>;
|
|
162
|
+
/** Throws Cx3Error INVALID_AMOUNT (not greater than 0) or INVALID_AMOUNT_PRECISION (more than 6 decimals) before signing */
|
|
159
163
|
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>;
|
|
160
164
|
bindApiKey(address?: string): Promise<any>;
|
|
161
165
|
}
|
|
@@ -191,19 +195,40 @@ export interface Cx3TokenDeployFields {
|
|
|
191
195
|
[k: string]: unknown;
|
|
192
196
|
}
|
|
193
197
|
|
|
198
|
+
/** A deploy answer: the gateway's fields plus dropped / finalAfter and wait() */
|
|
199
|
+
export interface Cx3DeployResult {
|
|
200
|
+
success?: boolean;
|
|
201
|
+
contractAddress?: string | null;
|
|
202
|
+
txHash?: string;
|
|
203
|
+
blockHeight?: number;
|
|
204
|
+
/** "pending" for a dropped deploy; wait() sets "confirmed" or "failed" */
|
|
205
|
+
status?: string;
|
|
206
|
+
/** trimmed from a block: not deployed yet, another validator may still include it until finalAfter */
|
|
207
|
+
dropped: boolean;
|
|
208
|
+
/** ms epoch after which a dropped deploy is failed for good; null otherwise */
|
|
209
|
+
finalAfter: number | null;
|
|
210
|
+
/** factory only: set once the deploy is confirmed (for a dropped deploy: by wait()) */
|
|
211
|
+
contract?: Contract;
|
|
212
|
+
/** factory, private deploy with authorizedWallets: [{ wallet, success, txHash?, error? }] */
|
|
213
|
+
authorized?: Array<{ wallet: string; success: boolean; txHash?: string | null; error?: string }>;
|
|
214
|
+
/** follows a pending / dropped deploy (until finalAfter by default); rejects only on a final failure (TX_FAILED) */
|
|
215
|
+
wait(opts?: { timeoutMs?: number; intervalMs?: number; onPending?: (status: any) => void; onConfirmed?: (status: any) => void }): Promise<any>;
|
|
216
|
+
[k: string]: any;
|
|
217
|
+
}
|
|
218
|
+
|
|
194
219
|
export class Cx3TokenFactory {
|
|
195
220
|
constructor(wallet: Cx3Wallet);
|
|
196
|
-
/** result.authorized: [{ wallet, success, txHash?, error? }] for a private deploy with authorizedWallets */
|
|
197
|
-
deployCX30(fields?: Cx3TokenDeployFields): Promise<
|
|
198
|
-
deployCX31(fields?: Cx3TokenDeployFields): Promise<
|
|
199
|
-
deployCX300(fields?: Cx3TokenDeployFields): Promise<
|
|
200
|
-
deployCX315(fields?: Cx3TokenDeployFields): Promise<
|
|
201
|
-
deployCX3192(fields?: Cx3TokenDeployFields): Promise<
|
|
202
|
-
deployCX337(fields?: object): Promise<
|
|
203
|
-
deployCX338(fields?: object): Promise<
|
|
204
|
-
deployCX339(fields?: object): Promise<
|
|
205
|
-
deployCX340(fields?: object): Promise<
|
|
206
|
-
deployCX341(fields?: object): Promise<
|
|
221
|
+
/** result.authorized: [{ wallet, success, txHash?, error? }] for a private deploy with authorizedWallets; a dropped deploy gets contract / authorized from result.wait() */
|
|
222
|
+
deployCX30(fields?: Cx3TokenDeployFields): Promise<Cx3DeployResult>;
|
|
223
|
+
deployCX31(fields?: Cx3TokenDeployFields): Promise<Cx3DeployResult>;
|
|
224
|
+
deployCX300(fields?: Cx3TokenDeployFields): Promise<Cx3DeployResult>;
|
|
225
|
+
deployCX315(fields?: Cx3TokenDeployFields): Promise<Cx3DeployResult>;
|
|
226
|
+
deployCX3192(fields?: Cx3TokenDeployFields): Promise<Cx3DeployResult>;
|
|
227
|
+
deployCX337(fields?: object): Promise<Cx3DeployResult>;
|
|
228
|
+
deployCX338(fields?: object): Promise<Cx3DeployResult>;
|
|
229
|
+
deployCX339(fields?: object): Promise<Cx3DeployResult>;
|
|
230
|
+
deployCX340(fields?: object): Promise<Cx3DeployResult>;
|
|
231
|
+
deployCX341(fields?: object): Promise<Cx3DeployResult>;
|
|
207
232
|
}
|
|
208
233
|
|
|
209
234
|
export class TransactionResponse {
|
|
@@ -214,8 +239,18 @@ export class TransactionResponse {
|
|
|
214
239
|
blockHeight?: number;
|
|
215
240
|
contractAddress: string | null;
|
|
216
241
|
async: boolean;
|
|
242
|
+
/** "pending" for async, TX_TIMEOUT (queued) and dropped answers; "confirmed" / "failed" for sync ones */
|
|
217
243
|
status: string;
|
|
218
|
-
|
|
244
|
+
/** trimmed from a block: not final, another validator may still include it until finalAfter */
|
|
245
|
+
dropped: boolean;
|
|
246
|
+
/** ms epoch after which a dropped tx is failed for good; null otherwise */
|
|
247
|
+
finalAfter: number | null;
|
|
248
|
+
/** the signed request's id (lookup: provider.getRequest) */
|
|
249
|
+
requestId: string | null;
|
|
250
|
+
hint: string | null;
|
|
251
|
+
error: string | null;
|
|
252
|
+
/** follows pending / dropped answers (until finalAfter by default); rejects only on a final failure */
|
|
253
|
+
wait(opts?: { timeoutMs?: number; intervalMs?: number; onPending?: (status: any) => void; onConfirmed?: (status: any) => void }): Promise<any>;
|
|
219
254
|
}
|
|
220
255
|
|
|
221
256
|
export function getDefaultProvider(options?: Cx3ProviderOptions & { url?: string }): Cx3Provider;
|
package/index.js
CHANGED
|
@@ -33,7 +33,7 @@ const {
|
|
|
33
33
|
} = require("./src/crypto");
|
|
34
34
|
const { normalizeAbi, argsToParams } = require("./src/abi");
|
|
35
35
|
|
|
36
|
-
const VERSION = "0.3.
|
|
36
|
+
const VERSION = "0.3.6";
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Convenience: provider from `options.url` or env `CX3_NODE_URL` — the CX3Api gateway
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carbonx3/cx3.js",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Official Node.js SDK (CX3js) for CarbonX3 Green Chain: OAuth2 app access, local secp256k1 signing, ABI-bound contracts and token deploys over REST + JSON",
|
|
5
5
|
"homepage": "https://cx3script.com",
|
|
6
6
|
"publishConfig": {
|
package/src/factory.js
CHANGED
|
@@ -63,8 +63,10 @@ class Cx3TokenFactory {
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* Deploy CX30 fungible token.
|
|
66
|
+
* A deploy not in a block yet (status "pending": dropped with `finalAfter`, or still queued) has no `contract` yet:
|
|
67
|
+
* `await result.wait()` follows it and adds `contract` (and `authorized`) once it is confirmed.
|
|
66
68
|
* @param {object} fields name, symbol, decimals, initialSupply, capabilities, target, …
|
|
67
|
-
* @returns {Promise<{ contractAddress, txHash, blockHeight, contract?: Contract }>}
|
|
69
|
+
* @returns {Promise<{ contractAddress, txHash, blockHeight, dropped, finalAfter, contract?: Contract, wait(opts?): Promise<object> }>}
|
|
68
70
|
*/
|
|
69
71
|
async deployCX30(fields = {}) {
|
|
70
72
|
return this._deployStandard("CX30", {
|
|
@@ -162,25 +164,43 @@ class Cx3TokenFactory {
|
|
|
162
164
|
...body,
|
|
163
165
|
standard
|
|
164
166
|
});
|
|
165
|
-
if (result && result.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
if (wallets.length) result.authorized = await this._authorize(result.contract, wallets);
|
|
167
|
+
if (result && (result.dropped || result.status === "pending") && typeof result.wait === "function") {
|
|
168
|
+
// trimmed from a block or still queued: not deployed yet. The contract handle (and the
|
|
169
|
+
// authorized wallets of a private token) come once result.wait() sees the deploy confirmed
|
|
170
|
+
const follow = result.wait;
|
|
171
|
+
let done = null;
|
|
172
|
+
const wait = async (opts) => {
|
|
173
|
+
const receipt = await follow(opts);
|
|
174
|
+
if (!done) done = this._finishDeploy(result, wallets);
|
|
175
|
+
await done;
|
|
176
|
+
return receipt;
|
|
177
|
+
};
|
|
178
|
+
Object.defineProperty(result, "wait", { value: wait, enumerable: false, configurable: true, writable: true });
|
|
179
|
+
return result;
|
|
180
180
|
}
|
|
181
|
+
if (result && result.success !== false && result.contractAddress) await this._finishDeploy(result, wallets);
|
|
181
182
|
return result;
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
// a deployed contract: its handle from the chain's ABI, then one addToWhitelist per authorized wallet
|
|
186
|
+
async _finishDeploy(result, wallets) {
|
|
187
|
+
if (!result.contractAddress) return;
|
|
188
|
+
try {
|
|
189
|
+
result.contract = await this.wallet.provider.getContract(
|
|
190
|
+
result.contractAddress,
|
|
191
|
+
null,
|
|
192
|
+
this.wallet
|
|
193
|
+
);
|
|
194
|
+
} catch (_) {
|
|
195
|
+
result.contract = new Contract(
|
|
196
|
+
result.contractAddress,
|
|
197
|
+
{ methods: {} },
|
|
198
|
+
this.wallet
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (wallets.length) result.authorized = await this._authorize(result.contract, wallets);
|
|
202
|
+
}
|
|
203
|
+
|
|
184
204
|
_privateFields(standard, fields, list) {
|
|
185
205
|
if (!PRIVATE_STANDARDS.has(standard)) {
|
|
186
206
|
throw new Cx3Error(`${standard} has no private variant (CX30, CX31, CX300, CX315 and CX3192 do)`, "PRIVATE_NOT_SUPPORTED");
|
package/src/provider.js
CHANGED
|
@@ -68,6 +68,15 @@ function withoutCredentialHeaders(headers) {
|
|
|
68
68
|
const CLIENT_ID_RE = /^cx3app_[a-f0-9]{24}$/i;
|
|
69
69
|
const CLIENT_SECRET_RE = /^cx3sec_[a-f0-9]{64}$/i;
|
|
70
70
|
|
|
71
|
+
// a dropped tx is failed for good after its finalAfter: wait this much past it (clock skew)
|
|
72
|
+
const FINAL_MARGIN_MS = 30000;
|
|
73
|
+
|
|
74
|
+
// no answer, a timeout, 429 or 5xx while polling: the tx may still be fine
|
|
75
|
+
function isTransientError(e) {
|
|
76
|
+
if (e instanceof Cx3HttpError) return e.status === 429 || e.status >= 500;
|
|
77
|
+
return !!e && (e.name === "AbortError" || e.name === "TimeoutError" || e.name === "TypeError");
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
class Cx3Provider {
|
|
72
81
|
/**
|
|
73
82
|
* @param {string} baseUrl CX3Api gateway URL (`CX3_NODE_URL`, e.g. https://cx3api.carbonx3.com), or a development node with `devNode`
|
|
@@ -574,11 +583,34 @@ class Cx3Provider {
|
|
|
574
583
|
...extras
|
|
575
584
|
};
|
|
576
585
|
delete body.sender;
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
586
|
+
try {
|
|
587
|
+
return await this.request("/contract/send", {
|
|
588
|
+
method: "POST",
|
|
589
|
+
body,
|
|
590
|
+
query: extras.wait === 0 ? { wait: 0 } : undefined
|
|
591
|
+
});
|
|
592
|
+
} catch (e) {
|
|
593
|
+
// TX_TIMEOUT as a 504 (a proxy, the /dapp/v1 group): queued, not final. Answered like the
|
|
594
|
+
// node's own 200 TX_TIMEOUT so tx.wait() follows the hash; without one it stays an error
|
|
595
|
+
if (e instanceof Cx3HttpError && e.code === "TX_TIMEOUT" && e.body && e.body.txHash) {
|
|
596
|
+
return { ...e.body, success: false, error: "TX_TIMEOUT" };
|
|
597
|
+
}
|
|
598
|
+
throw e;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* What became of a signed contract send, by the `requestId` every send answer carries
|
|
604
|
+
* (`/dapp/v1/requests/:requestId?from=`, public): `{ status: "executed" | "pending" | "submitted" |
|
|
605
|
+
* "unknown", txHash?, blockHeight?, outcome?, expiresAt? }`. After a lost answer or REPLAYED_REQUEST,
|
|
606
|
+
* look the request up here instead of signing again; 503 REQUEST_LOOKUP_UNAVAILABLE = ask later.
|
|
607
|
+
*/
|
|
608
|
+
async getRequest(requestId, from) {
|
|
609
|
+
const id = String(requestId || "").toLowerCase();
|
|
610
|
+
if (!/^[0-9a-f]{64}$/.test(id)) throw new Cx3Error("request id must be 64 hex characters", "INVALID_REQUEST_ID");
|
|
611
|
+
const addr = normalizeAddress(from);
|
|
612
|
+
if (!addr) throw new Cx3Error("address required", "NO_ADDRESS");
|
|
613
|
+
return this._dappRead(`/requests/${id}`, { from: addr });
|
|
582
614
|
}
|
|
583
615
|
|
|
584
616
|
async getTxStatus(txHash) {
|
|
@@ -600,15 +632,34 @@ class Cx3Provider {
|
|
|
600
632
|
|
|
601
633
|
/**
|
|
602
634
|
* Poll until confirmed / failed / timeout.
|
|
635
|
+
* A dropped tx (`dropped`, `finalAfter` in the send or status answer) is not final: another
|
|
636
|
+
* validator may still include it until finalAfter, so by default the wait lasts until then
|
|
637
|
+
* (+ FINAL_MARGIN_MS); `opts.timeoutMs` sets the caller's own limit. Only a final failure
|
|
638
|
+
* rejects with TX_FAILED; TX_TRACK_TIMEOUT means "still pending", not "failed".
|
|
639
|
+
* @param {{ timeoutMs?: number, intervalMs?: number, finalAfter?: number, onPending?: Function, onConfirmed?: Function }} [opts]
|
|
603
640
|
* @returns {Promise<object>} terminal receipt
|
|
604
641
|
*/
|
|
605
642
|
async waitForTransaction(txHash, opts = {}) {
|
|
606
|
-
const timeoutMs = opts.timeoutMs || 60000;
|
|
607
|
-
let interval = opts.intervalMs || 1000;
|
|
608
643
|
const start = Date.now();
|
|
609
|
-
|
|
644
|
+
let finalAfter = Number(opts.finalAfter) > 0 ? Number(opts.finalAfter) : null;
|
|
645
|
+
let dropped = finalAfter !== null;
|
|
646
|
+
const deadline = () => (opts.timeoutMs > 0
|
|
647
|
+
? start + opts.timeoutMs
|
|
648
|
+
: Math.max(start + 60000, finalAfter !== null ? finalAfter + FINAL_MARGIN_MS : 0));
|
|
649
|
+
let interval = opts.intervalMs || 1000;
|
|
650
|
+
while (Date.now() < deadline()) {
|
|
610
651
|
await sleep(interval);
|
|
611
|
-
|
|
652
|
+
let status;
|
|
653
|
+
try {
|
|
654
|
+
status = await this.getTxStatus(txHash);
|
|
655
|
+
} catch (e) {
|
|
656
|
+
// a network blip or a busy gateway says nothing about the tx: ask again
|
|
657
|
+
if (!isTransientError(e)) throw e;
|
|
658
|
+
interval = Math.min(5000, Math.round(interval * 1.5));
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (status.dropped === true) dropped = true;
|
|
662
|
+
if (Number(status.finalAfter) > 0) finalAfter = Number(status.finalAfter);
|
|
612
663
|
if (typeof opts.onPending === "function" && status.status === "pending") {
|
|
613
664
|
try { opts.onPending(status); } catch (_) { /* ignore */ }
|
|
614
665
|
}
|
|
@@ -623,7 +674,8 @@ class Cx3Provider {
|
|
|
623
674
|
}
|
|
624
675
|
interval = Math.min(5000, Math.round(interval * 1.5));
|
|
625
676
|
}
|
|
626
|
-
|
|
677
|
+
// not final: never a reason to sign the same call again
|
|
678
|
+
const err = new Cx3TxError("TX_TRACK_TIMEOUT", "TX_TRACK_TIMEOUT", { txHash, status: "pending", dropped, finalAfter });
|
|
627
679
|
err.txHash = txHash;
|
|
628
680
|
throw err;
|
|
629
681
|
}
|
package/src/transaction.js
CHANGED
|
@@ -18,25 +18,34 @@ class TransactionResponse {
|
|
|
18
18
|
constructor(result, provider = null, opts = {}) {
|
|
19
19
|
this.raw = result || {};
|
|
20
20
|
this.provider = provider;
|
|
21
|
-
this.success = this.raw.success !== false;
|
|
22
21
|
this.txHash = this.raw.txHash || this.raw.hash || null;
|
|
23
22
|
this.hash = this.txHash;
|
|
23
|
+
// TX_TIMEOUT with a hash: queued, not final within the node's sync wait (25 s), so pending:
|
|
24
|
+
// follow it with wait(), never sign the same call again
|
|
25
|
+
const queued = this.raw.success === false && this.raw.error === "TX_TIMEOUT" && !!this.txHash;
|
|
26
|
+
this.success = this.raw.success !== false || queued;
|
|
24
27
|
this.blockHeight = this.raw.blockHeight;
|
|
25
28
|
this.contractAddress = this.raw.contractAddress || null;
|
|
26
29
|
this.async = !!this.raw.async || opts.wait === 0;
|
|
27
|
-
|
|
28
|
-
this.
|
|
30
|
+
// trimmed from a block (202 dropped): not final, another validator may still include it until finalAfter
|
|
31
|
+
this.dropped = this.raw.dropped === true;
|
|
32
|
+
this.finalAfter = Number(this.raw.finalAfter) > 0 ? Number(this.raw.finalAfter) : null;
|
|
33
|
+
this.requestId = this.raw.requestId || null;
|
|
34
|
+
this.hint = this.raw.hint || null;
|
|
35
|
+
this.status = queued ? "pending" : (this.raw.status || (this.async ? "pending" : (this.success ? "confirmed" : "failed")));
|
|
36
|
+
this.error = queued ? null : (this.raw.error || this.raw.message || null);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/**
|
|
32
|
-
* Wait for confirmation when the node returned async / pending.
|
|
33
|
-
* Sync responses that already include blockHeight resolve immediately.
|
|
40
|
+
* Wait for confirmation when the node returned async / pending (also TX_TIMEOUT and dropped).
|
|
41
|
+
* Sync responses that already include blockHeight resolve immediately. A dropped tx is followed
|
|
42
|
+
* until its finalAfter by default; only a final failure rejects (TX_FAILED).
|
|
34
43
|
*/
|
|
35
44
|
async wait(opts = {}) {
|
|
36
45
|
if (!this.success) {
|
|
37
46
|
throw new Cx3TxError(this.error || "TX_FAILED", this.error || "TX_FAILED", this.raw);
|
|
38
47
|
}
|
|
39
|
-
if (!this.async && this.blockHeight != null) {
|
|
48
|
+
if (!this.async && !this.dropped && this.blockHeight != null) {
|
|
40
49
|
return {
|
|
41
50
|
status: "confirmed",
|
|
42
51
|
txHash: this.txHash,
|
|
@@ -50,7 +59,7 @@ class TransactionResponse {
|
|
|
50
59
|
if (!this.provider) {
|
|
51
60
|
throw new Cx3TxError("No provider to wait on transaction", "NO_PROVIDER", this.raw);
|
|
52
61
|
}
|
|
53
|
-
return this.provider.waitForTransaction(this.txHash, opts);
|
|
62
|
+
return this.provider.waitForTransaction(this.txHash, this.finalAfter ? { finalAfter: this.finalAfter, ...opts } : opts);
|
|
54
63
|
}
|
|
55
64
|
}
|
|
56
65
|
|
package/src/wallet.js
CHANGED
|
@@ -23,6 +23,39 @@ function isValidTokenSymbol(symbol) {
|
|
|
23
23
|
return TOKEN_SYMBOL_RE.test(String(symbol == null ? "" : symbol));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* A deploy result keeps the gateway's fields and gets `dropped`, `finalAfter` and `wait()`. A deploy
|
|
28
|
+
* the node trimmed from its block (202 dropped, status "pending") is not deployed yet: another
|
|
29
|
+
* validator may still include it until finalAfter. One still queued after the node's 25 s wait (202
|
|
30
|
+
* `queued`, or an older gateway's 200 TX_TIMEOUT with a hash) is pending too, status "pending".
|
|
31
|
+
* wait() follows it like TransactionResponse.wait() (TX_FAILED only when final) and sets `status`
|
|
32
|
+
* (and blockHeight / contractAddress) from the answer.
|
|
33
|
+
*/
|
|
34
|
+
function withDeployWait(result, provider) {
|
|
35
|
+
if (!result || typeof result !== "object") return result;
|
|
36
|
+
const tx = new TransactionResponse(result, provider);
|
|
37
|
+
result.dropped = tx.dropped;
|
|
38
|
+
result.finalAfter = tx.finalAfter;
|
|
39
|
+
if (tx.status === "pending" && result.status === undefined) result.status = "pending";
|
|
40
|
+
if (tx.status === "pending" && result.contractAddress === undefined) result.contractAddress = null;
|
|
41
|
+
const wait = async (opts) => {
|
|
42
|
+
let receipt;
|
|
43
|
+
try {
|
|
44
|
+
receipt = await tx.wait(opts);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
if (e && e.code === "TX_FAILED") result.status = "failed";
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
result.status = "confirmed";
|
|
50
|
+
if (result.blockHeight == null && receipt && receipt.blockHeight != null) result.blockHeight = receipt.blockHeight;
|
|
51
|
+
if (!result.contractAddress && receipt && receipt.contractAddress) result.contractAddress = receipt.contractAddress;
|
|
52
|
+
return receipt;
|
|
53
|
+
};
|
|
54
|
+
// not enumerable: the result still serializes (and spreads) as the gateway's answer
|
|
55
|
+
Object.defineProperty(result, "wait", { value: wait, enumerable: false, configurable: true, writable: true });
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
26
59
|
/**
|
|
27
60
|
* Local secp256k1 wallet for Green Chain digests.
|
|
28
61
|
*/
|
|
@@ -146,7 +179,8 @@ class Cx3Wallet {
|
|
|
146
179
|
/**
|
|
147
180
|
* Deploy via Green Chain `POST /deployContract`. A symbol outside A-Z / 0-9 is refused here,
|
|
148
181
|
* before signing (INVALID_TOKEN_SYMBOL); the chain also refuses a symbol already in use
|
|
149
|
-
* (Cx3HttpError code DUPLICATE_TOKEN_SYMBOL, status 409).
|
|
182
|
+
* (Cx3HttpError code DUPLICATE_TOKEN_SYMBOL, status 409). A dropped deploy (202, `dropped`,
|
|
183
|
+
* `finalAfter`) is pending, not deployed: `result.wait()` follows it (withDeployWait).
|
|
150
184
|
*/
|
|
151
185
|
async deployContract(fields) {
|
|
152
186
|
if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
|
|
@@ -179,7 +213,7 @@ class Cx3Wallet {
|
|
|
179
213
|
method: "POST",
|
|
180
214
|
body
|
|
181
215
|
});
|
|
182
|
-
return result;
|
|
216
|
+
return withDeployWait(result, this.provider);
|
|
183
217
|
}
|
|
184
218
|
|
|
185
219
|
async _ownerActionRequest(action, address, owner, method, path) {
|
|
@@ -229,6 +263,8 @@ class Cx3Wallet {
|
|
|
229
263
|
/**
|
|
230
264
|
* Native CX3 coin transfer (`POST /sendTransaction`).
|
|
231
265
|
* Distinct from `sendTransaction()` which is a signed `/contract/send`.
|
|
266
|
+
* The amount is checked before signing: greater than 0 (INVALID_AMOUNT), at most 6 decimals
|
|
267
|
+
* (INVALID_AMOUNT_PRECISION).
|
|
232
268
|
*/
|
|
233
269
|
async transferNative(to, amount, opts = {}) {
|
|
234
270
|
if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
|
|
@@ -238,6 +274,10 @@ class Cx3Wallet {
|
|
|
238
274
|
if (!Number.isFinite(amt) || amt <= 0) {
|
|
239
275
|
throw new Cx3Error("Invalid amount", "INVALID_AMOUNT");
|
|
240
276
|
}
|
|
277
|
+
// CX3 has 6 decimals: the chain refuses a finer amount (the node's fitsSubunits rule), so it is not signed
|
|
278
|
+
if (Number(amt.toFixed(6)) !== amt) {
|
|
279
|
+
throw new Cx3Error("CX3 amounts have at most 6 decimals", "INVALID_AMOUNT_PRECISION", { maxDecimals: 6 });
|
|
280
|
+
}
|
|
241
281
|
let nonce = opts.nonce;
|
|
242
282
|
if (nonce == null) {
|
|
243
283
|
try {
|