@neuraiproject/neurai-assets 1.4.1 → 1.5.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 CHANGED
@@ -484,18 +484,142 @@ All creation/reissuance operations return an object with this structure:
484
484
 
485
485
  ```javascript
486
486
  {
487
- rawTx: 'hex string', // Unsigned transaction (to sign with wallet)
487
+ rawTx: 'hex string', // Unsigned transaction built by the node (to sign with wallet)
488
488
  utxos: [...], // UTXOs selected for the operation
489
489
  inputs: [...], // Transaction inputs
490
- outputs: [...], // Ordered outputs
490
+ outputs: [...], // Ordered outputs (DISPLAY amounts, for createrawtransaction)
491
491
  fee: 0.001, // Fee in XNA
492
492
  burnAmount: 1000, // Burned amount in XNA
493
493
  assetName: 'MYTOKEN', // Operation-specific fields vary by builder
494
494
  ownerTokenName: 'MYTOKEN!',
495
- operationType: 'ISSUE_ROOT'
495
+ operationType: 'ISSUE_ROOT',
496
+ createTransactionBuild: { ... } // Build it yourself, offline — see below
496
497
  }
497
498
  ```
498
499
 
500
+ ## Building offline with `createTransactionBuild` (1.5.0+)
501
+
502
+ `rawTx` above is built by the node through `createrawtransaction`. To build the
503
+ same transaction **locally**, pass `result.createTransactionBuild` straight to
504
+ `@neuraiproject/neurai-create-transaction`:
505
+
506
+ ```javascript
507
+ import { createFromOperation } from '@neuraiproject/neurai-create-transaction';
508
+
509
+ const result = await assets.transferAsset({
510
+ assetName: 'MYTOKEN',
511
+ recipients: [{ address: 'tRecipient...', amount: 4.35 }]
512
+ });
513
+
514
+ const built = createFromOperation(result.createTransactionBuild);
515
+ // built.rawTx — sign it with @neuraiproject/neurai-sign-transaction
516
+ ```
517
+
518
+ Nothing has to be renamed, rescaled or reinterpreted in between. In particular
519
+ you do **not** need to:
520
+
521
+ - map the operation type — a transfer already arrives as `STANDARD_TRANSFER` or
522
+ `TRANSFER_DEPIN`, the discriminants the serializer accepts;
523
+ - merge recipients, asset change and the DePIN owner return into one list (the
524
+ serializer emits the `&NAME!` escort itself; listing it again would produce
525
+ two owner outputs);
526
+ - convert display amounts to protocol integers;
527
+ - add or correct `assetMarker`.
528
+
529
+ ### Display amounts vs raw amounts
530
+
531
+ The library speaks two representations, deliberately kept apart:
532
+
533
+ | Where | Representation | Why |
534
+ | --- | --- | --- |
535
+ | Your call (`quantity`, `recipients[].amount`) | Display (`4.35`) | What a user types |
536
+ | `result.outputs` (RPC envelope) | Display (`4.35`) | `createrawtransaction` scales it itself |
537
+ | `result.createTransactionBuild` (`*Raw`, `*Sats`) | `bigint` (`435000000n`) | What the chain encodes |
538
+
539
+ The asset payload scale is always `10^8`, **independently of the asset's
540
+ `units`**: `units` limits divisibility and presentation, it is never a
541
+ multiplier. A quantity of `1.25` reaches the chain as `125000000n` whether the
542
+ asset has `units=2` or `units=8`.
543
+
544
+ Conversion goes through text rather than `Math.round(value * 1e8)`. That
545
+ multiplication is fine for ordinary magnitudes — `4.35 * 1e8` is
546
+ `434999999.99999994`, but `Math.round` recovers `435000000` — and it fails
547
+ silently in exactly two places:
548
+
549
+ - **more than eight decimals** are rounded away instead of rejected, so an
550
+ amount can vanish (`1e-9` → `0`) or shift (`1.123456789` → `112345679`);
551
+ - **past `Number.MAX_SAFE_INTEGER`** the scaled product drops bits:
552
+ `184467440.73709551` → `18446744073709552`, one unit off.
553
+
554
+ Both are reachable with values a wallet can hold. Here, an amount is rejected
555
+ rather than silently truncated when it has more than eight decimals, when the
556
+ asset's `units` cannot represent it, or when it exceeds the consensus ceiling
557
+ `MAX_MONEY` (`21000000000` units — the node's `MoneyRange`).
558
+
559
+ ### Large amounts: pass a string
560
+
561
+ Above `MAX_SAFE_INTEGER / 1e8` (~`90071992.55`) a JavaScript `number` can no
562
+ longer name every 8-decimal value, so a **fractional** one is refused there and
563
+ the error names the string to use instead:
564
+
565
+ ```javascript
566
+ await assets.createRootAsset({ assetName: 'BIG', quantity: 100000000.5, units: 1 });
567
+ // InvalidAmountError: ... pass it as a decimal string ("100000000.5") instead.
568
+
569
+ await assets.createRootAsset({ assetName: 'BIG', quantity: '100000000.5', units: 1 });
570
+ // works
571
+ ```
572
+
573
+ A **safe integer** is still accepted however large it scales, so the documented
574
+ maximum supply `21000000000` keeps working as a number. Strings are exempt from
575
+ this precision guard — they carried their own digits — but not from any other
576
+ rule: sign, decimals, `units` divisibility and `MAX_MONEY` apply equally.
577
+
578
+ ### Migration 1.4.x → 1.5.x → 2.0
579
+
580
+ | | 1.4.x | 1.5.x | 2.0 |
581
+ | --- | --- | --- | --- |
582
+ | `localRawBuild` | only option | present, **deprecated** | removed |
583
+ | `createTransactionBuild` | — | present, canonical | only option |
584
+ | Transfers via `createFromOperation` | rejected (`TRANSFER` is not a discriminant) | work | work |
585
+ | `*Raw` fields | display values | protocol integers | protocol integers |
586
+ | `toSatoshis(amount, units)` | returns the display amount | unchanged, deprecated | removed |
587
+
588
+ `localRawBuild` keeps its exact 1.4.x shape through the whole 1.x line, so no
589
+ consumer has to move on this release. New integrations should read
590
+ `createTransactionBuild` only.
591
+
592
+ ### Serializer version
593
+
594
+ The canonical contract needs `@neuraiproject/neurai-create-transaction`
595
+ **>= 0.8.0**. Two of its fixes are load-bearing here:
596
+
597
+ - global `FREEZE_ASSET` / `UNFREEZE_ASSET` encode the restriction flag as
598
+ `1`/`0` (0.7.0 emitted `3`/`2`, which the node rejected with
599
+ `bad-txns-null-data-flag-must-be-0-or-1`, so those two discriminants could
600
+ not reach a mempool);
601
+ - a reissue that omits `units` encodes "keep the current units" (`0xff`), which
602
+ is what this library relies on — see below.
603
+
604
+ ### Reissue never changes an asset's units
605
+
606
+ There is no API here to change the precision of an existing asset, so a reissue
607
+ build deliberately **omits** `units`, which the serializer encodes as `0xff`
608
+ ("keep"). Echoing the value read from `getassetdata` would instead say "set
609
+ units to N", and a stale read — the asset reissued to a higher precision
610
+ between the read and the broadcast — would ask the node to lower them, which it
611
+ rejects with `unit must be larger than current unit selection`.
612
+
613
+ The value read from the chain is still used, to check that the requested
614
+ `quantity` fits the asset's precision.
615
+
616
+ One consequence worth knowing: the **node-built** `rawTx` cannot reissue an
617
+ asset whose `units` are above zero at all. `createrawtransaction`'s `reissue`
618
+ object has no field for units and the node fills in `0`, so it refuses. That is
619
+ a limitation of the RPC interface, not of the operation — build those offline
620
+ with `createFromOperation(result.createTransactionBuild)`. The error message
621
+ says so when you hit it.
622
+
499
623
  ## Owner Tokens - IMPORTANT
500
624
 
501
625
  When you create an asset, an **owner token** is automatically generated (e.g., `MYTOKEN!`).
@@ -544,18 +668,43 @@ Ravencoin-inherited `rvn` to `xna` at an activation height per network
544
668
 
545
669
  - Transactions built **through the node** (`createrawtransaction`) need
546
670
  nothing: the node stamps the marker itself.
547
- - The `localRawBuild` metadata (consumed by
548
- `@neuraiproject/neurai-create-transaction createFromOperation`, >= 0.7.0)
549
- now carries `params.assetMarker`. Builders resolve it once per build:
671
+ - Locally built transactions carry `params.assetMarker` in both
672
+ `createTransactionBuild` and the deprecated `localRawBuild`. Builders
673
+ resolve it **once per build**:
550
674
  1. `params.assetMarker` / `config.assetMarker` if you set it (`'rvn'` |
551
675
  `'xna'` — offline builds or tests);
552
676
  2. otherwise the node's `getblockchaininfo.asset_marker` (node commit
553
677
  `347362b` or later);
554
- 3. `'rvn'` when the node predates that field or the call fails which
555
- matches what such a node enforces.
678
+ 3. `'rvn'` when the node predates that field which matches what such a
679
+ node enforces.
556
680
 
557
681
  No height tables and no network inference: the node (or you) decides.
558
682
 
683
+ ### Failure policy (`assetMarkerPolicy`, 1.5.0+)
684
+
685
+ Step 3 above covers a node that *answers* without the field. A node that does
686
+ not answer at all is a different situation, and `assetMarkerPolicy` decides it:
687
+
688
+ ```javascript
689
+ const assets = new NeuraiAssets(rpc, {
690
+ network: 'xna-test',
691
+ addresses: [...],
692
+ assetMarkerPolicy: 'strict' // default: 'legacy-fallback'
693
+ });
694
+ ```
695
+
696
+ | Policy | `getblockchaininfo` fails | Field absent/null | Unknown value |
697
+ | --- | --- | --- | --- |
698
+ | `legacy-fallback` (default in 1.x) | resolves `'rvn'` | resolves `'rvn'` | throws |
699
+ | `strict` | **throws** | resolves `'rvn'` | throws |
700
+
701
+ Use `strict` in a connected wallet on a post-NIP-040 chain: guessing `'rvn'`
702
+ there builds a transaction the node rejects with
703
+ `bad-txns-legacy-asset-marker-after-nip040`, so "the node did not answer" must
704
+ not silently become "the node said rvn". The rejection propagates out of the
705
+ build — you never receive a partial result — and the node is queried only once,
706
+ whether the query succeeds or fails.
707
+
559
708
  ## Validations
560
709
 
561
710
  The library validates client-side:
@@ -744,11 +893,15 @@ are `xna` and `xna-test`; `xna-pq` and `xna-pq-test` remain available as compati
744
893
 
745
894
  ## Fee estimation (PQ-aware)
746
895
 
747
- Asset transactions are usually built with one or two XNA inputs plus, depending on the operation, an owner-token or qualifier UTXO. The library estimates the fee twice per build: a rough pre-estimate to size the initial XNA selection, and a final estimate once the actual UTXOs are known.
896
+ Asset transactions are usually built with one or two XNA inputs plus, depending on the operation, an owner-token or qualifier UTXO. Since `1.5.0` the XNA side is funded by a loop that selects inputs, recomputes the fee from the *real* (PQ-aware) descriptors of the full input set, and repeats until the funds cover burn + fee. Every round excludes the outpoints it already holds, so a transaction never spends the same outpoint twice and the fee always accounts for every input it pays for.
748
897
 
749
- Both estimates share a single `estimatesmartfee` lookup. The fee rate is stable for the lifetime of one build, so it is fetched on the first `estimateFee` call and cached on the builder instance for the second half as many RPC round trips as before `1.3.1`.
898
+ Running out of funds raises `InsufficientFundsError` rather than returning an underfunded build. That includes a case worth knowing about: each PQ input costs about `0.0147 XNA` in fee, so a UTXO worth less than that makes the shortfall *worse*, and a wallet fragmented into such pieces cannot fund a PQ transaction at all.
750
899
 
751
- Both estimates use the helpers in [`src/utils/feeSizing.js`](src/utils/feeSizing.js) and distinguish PQ AuthScript inputs/outputs from legacy P2PKH ones. PQ inputs spend ~977 vbytes vs ~148 for legacy without this distinction, transactions built from PQ addresses fall under the node's `min relay fee` and are rejected with `code -26: min relay fee not met`.
900
+ All estimates share a single `estimatesmartfee` lookup. The fee rate is stable for the lifetime of one build, so it is fetched on the first estimate and cached on the builder instance.
901
+
902
+ Estimates use the helpers in [`src/utils/feeSizing.js`](src/utils/feeSizing.js) and distinguish PQ AuthScript inputs/outputs from legacy P2PKH ones. PQ inputs spend ~977 vbytes vs ~148 for legacy — without this distinction, transactions built from PQ addresses fall under the node's `min relay fee` and are rejected with `code -26: min relay fee not met`.
903
+
904
+ Outputs that carry an asset payload are sized as such, not as bare P2PKH outputs. An asset output is `<destination> OP_XNA_ASSET <pushdata payload> OP_DROP`, which adds roughly 20-60 bytes; ignoring that under-counts a transaction by a few percent, and that is enough to fall below the floor whenever the node's fee rate sits close to its minimum relay fee.
752
905
 
753
906
  You should not need to call these helpers directly; they are wired into every builder. They are documented here so you can audit the fee math or use the same constants if you compose transactions outside the standard builder flow.
754
907
 
@@ -772,6 +925,12 @@ estimateInputVbytes({ address: 'nq1…' }); // 977
772
925
  estimateInputVbytes({ address: 'mgRYHdMq…' }); // 148
773
926
  estimateOutputBytes('tnq1…'); // 43
774
927
 
928
+ // Asset outputs declare their payload: kind is 'transfer' (default),
929
+ // 'owner', 'issue' or 'reissue'.
930
+ estimateOutputBytes({ address: 't7pv…', assetName: 'ROOTX' }); // 55
931
+ estimateOutputBytes({ address: 't7pv…', assetName: 'ROOTX!', kind: 'owner' }); // 48
932
+ estimateOutputBytes({ address: 't7pv…', assetName: 'ROOTX', kind: 'issue' }); // 58
933
+
775
934
  const vbytes = estimateTransactionVbytes(
776
935
  [{ script: '5120…' }, { address: 'mgRYHdMq…' }], // 1 PQ + 1 legacy input
777
936
  ['nq1qchange…', 'mgRYHdMqburn…'], // 1 PQ + 1 legacy output