@carbonx3/cx3.js 0.3.4 → 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 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
 
@@ -145,12 +146,27 @@ Supported ABI shapes:
145
146
 
146
147
  ## Transactions and fees
147
148
 
148
- Every transaction pays gas in CX3 to the validator that produces the block: gas used × gas price (devnet default 0.0001 CX3 per gas).
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
- - A native transfer uses 21,000 gas: **2.1 CX3**.
151
- - A contract send or deploy is admitted only when the sender holds its gas limit × gas price (100,000 gas by default, so at least **10 CX3**) and is charged the gas it used.
151
+ - A native transfer uses 21,000 gas: **2.1 CX3**. The sender needs the amount plus the fee.
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 fails with `INSUFFICIENT_BALANCE`, and a gas price below the chain floor with `GAS_PRICE_TOO_LOW`. A send waits for its block by default; with `{ wait: 0 }` it returns at once, and `tx.wait()` polls the transaction status until it is confirmed or failed.
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. Signatures carry a timestamp and are accepted within ±5 minutes of the node's clock; a native transfer is taken only by a block within 10 minutes of its timestamp. The genesis hash ties a native transfer to one network and one genesis: after a chain reset, a transfer signed before it no longer verifies. An address is `0x` + the last 20 bytes of `sha256(uncompressedPublicKey)`.
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`; `Cx3TxError` adds the transaction `receipt`.
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");
@@ -233,16 +251,26 @@ try {
233
251
  | `invalid_client` | The token endpoint refused the application (wrong secret, or revoked) |
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
- | `INSUFFICIENT_BALANCE`, `GAS_PRICE_TOO_LOW` | The sender cannot pay the gas, or the gas price is below the chain floor |
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
- | `TX_FAILED`, `TX_TRACK_TIMEOUT` | The transaction failed, or `tx.wait()` did not see it confirmed in time |
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.7.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.
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):
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
- waitForTransaction(txHash: string, opts?: object): Promise<any>;
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<any>;
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<any>;
198
- deployCX31(fields?: Cx3TokenDeployFields): Promise<any>;
199
- deployCX300(fields?: Cx3TokenDeployFields): Promise<any>;
200
- deployCX315(fields?: Cx3TokenDeployFields): Promise<any>;
201
- deployCX3192(fields?: Cx3TokenDeployFields): Promise<any>;
202
- deployCX337(fields?: object): Promise<any>;
203
- deployCX338(fields?: object): Promise<any>;
204
- deployCX339(fields?: object): Promise<any>;
205
- deployCX340(fields?: object): Promise<any>;
206
- deployCX341(fields?: object): Promise<any>;
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
- wait(opts?: object): Promise<any>;
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.4";
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.4",
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.success !== false && result.contractAddress) {
166
- try {
167
- result.contract = await this.wallet.provider.getContract(
168
- result.contractAddress,
169
- null,
170
- this.wallet
171
- );
172
- } catch (_) {
173
- result.contract = new Contract(
174
- result.contractAddress,
175
- { methods: {} },
176
- this.wallet
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`
@@ -502,7 +511,8 @@ class Cx3Provider {
502
511
  /**
503
512
  * Check that this wallet is the one the credentials belong to (an app and a wallet API key are
504
513
  * both bound to one wallet): `{ ok: true }`, or `wallet_mismatch`.
505
- * OAuth2 app → GET `/wallet/api-key/bind`; cx3wk_ key → GET `/whoami`, then `/wallet/api-key/bind` on 404.
514
+ * OAuth2 app and cx3wk_ key → GET `/wallet/api-key/bind` (the gateway route; it has no `/whoami`);
515
+ * a cx3wk_ key falls back to `/whoami` on 404 (a same-origin proxy that only forwards that).
506
516
  * A development node has no credentials: `{ ok: true, devNode: true }`.
507
517
  */
508
518
  async bindApiKey(address) {
@@ -518,11 +528,11 @@ class Cx3Provider {
518
528
  return { ok: true, devNode: true };
519
529
  }
520
530
  try {
521
- const json = await this.request("/whoami", { query });
531
+ const json = await this.request("/wallet/api-key/bind", { query });
522
532
  return this._rejectEnvelope(json);
523
533
  } catch (e) {
524
534
  if (e && e.status === 404) {
525
- const json = await this.request("/wallet/api-key/bind", { query });
535
+ const json = await this.request("/whoami", { query });
526
536
  return this._rejectEnvelope(json);
527
537
  }
528
538
  throw e;
@@ -573,11 +583,34 @@ class Cx3Provider {
573
583
  ...extras
574
584
  };
575
585
  delete body.sender;
576
- return this.request("/contract/send", {
577
- method: "POST",
578
- body,
579
- query: extras.wait === 0 ? { wait: 0 } : undefined
580
- });
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 });
581
614
  }
582
615
 
583
616
  async getTxStatus(txHash) {
@@ -599,15 +632,34 @@ class Cx3Provider {
599
632
 
600
633
  /**
601
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]
602
640
  * @returns {Promise<object>} terminal receipt
603
641
  */
604
642
  async waitForTransaction(txHash, opts = {}) {
605
- const timeoutMs = opts.timeoutMs || 60000;
606
- let interval = opts.intervalMs || 1000;
607
643
  const start = Date.now();
608
- while (Date.now() - start < timeoutMs) {
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()) {
609
651
  await sleep(interval);
610
- const status = await this.getTxStatus(txHash);
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);
611
663
  if (typeof opts.onPending === "function" && status.status === "pending") {
612
664
  try { opts.onPending(status); } catch (_) { /* ignore */ }
613
665
  }
@@ -622,7 +674,8 @@ class Cx3Provider {
622
674
  }
623
675
  interval = Math.min(5000, Math.round(interval * 1.5));
624
676
  }
625
- const err = new Cx3TxError("TX_TRACK_TIMEOUT", "TX_TRACK_TIMEOUT", { txHash });
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 });
626
679
  err.txHash = txHash;
627
680
  throw err;
628
681
  }
@@ -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
- this.status = this.raw.status || (this.async ? "pending" : (this.success ? "confirmed" : "failed"));
28
- this.error = this.raw.error || this.raw.message || null;
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 {