@neuraiproject/neurai-assets 1.4.1 → 1.5.1

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,156 @@ 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
+ ### Reissue is built locally
617
+
618
+ `createrawtransaction`'s `reissue` object has no field for units, so the node
619
+ fills in `0` and refuses any asset whose units are above zero
620
+ (`unit must be larger than current unit selection`). Since 1.5.0 the two
621
+ reissue operations therefore skip that RPC and build their `rawTx` with
622
+ `createFromOperation`, which can say "keep the current units". They report
623
+ `buildStrategy: 'local-builder'`; every other operation still reports
624
+ `'rpc-node'`.
625
+
626
+ This is why `@neuraiproject/neurai-create-transaction` is a runtime
627
+ **dependency**, not just a dev one.
628
+
629
+ One consequence to know about: on the local path `result.outputs` and `rawTx`
630
+ describe the same operation but not the same output list. The node
631
+ auto-generates the owner-token return while processing a reissue entry, so the
632
+ RPC envelope omits it, while the locally built transaction carries it
633
+ explicitly — three entries in `outputs` against four vouts in `rawTx`. Both are
634
+ valid; parse `rawTx` when you need the outputs the chain will see, and do not
635
+ index `outputs` against its vouts.
636
+
499
637
  ## Owner Tokens - IMPORTANT
500
638
 
501
639
  When you create an asset, an **owner token** is automatically generated (e.g., `MYTOKEN!`).
@@ -544,18 +682,43 @@ Ravencoin-inherited `rvn` to `xna` at an activation height per network
544
682
 
545
683
  - Transactions built **through the node** (`createrawtransaction`) need
546
684
  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:
685
+ - Locally built transactions carry `params.assetMarker` in both
686
+ `createTransactionBuild` and the deprecated `localRawBuild`. Builders
687
+ resolve it **once per build**:
550
688
  1. `params.assetMarker` / `config.assetMarker` if you set it (`'rvn'` |
551
689
  `'xna'` — offline builds or tests);
552
690
  2. otherwise the node's `getblockchaininfo.asset_marker` (node commit
553
691
  `347362b` or later);
554
- 3. `'rvn'` when the node predates that field or the call fails which
555
- matches what such a node enforces.
692
+ 3. `'rvn'` when the node predates that field which matches what such a
693
+ node enforces.
556
694
 
557
695
  No height tables and no network inference: the node (or you) decides.
558
696
 
697
+ ### Failure policy (`assetMarkerPolicy`, 1.5.0+)
698
+
699
+ Step 3 above covers a node that *answers* without the field. A node that does
700
+ not answer at all is a different situation, and `assetMarkerPolicy` decides it:
701
+
702
+ ```javascript
703
+ const assets = new NeuraiAssets(rpc, {
704
+ network: 'xna-test',
705
+ addresses: [...],
706
+ assetMarkerPolicy: 'strict' // default: 'legacy-fallback'
707
+ });
708
+ ```
709
+
710
+ | Policy | `getblockchaininfo` fails | Field absent/null | Unknown value |
711
+ | --- | --- | --- | --- |
712
+ | `legacy-fallback` (default in 1.x) | resolves `'rvn'` | resolves `'rvn'` | throws |
713
+ | `strict` | **throws** | resolves `'rvn'` | throws |
714
+
715
+ Use `strict` in a connected wallet on a post-NIP-040 chain: guessing `'rvn'`
716
+ there builds a transaction the node rejects with
717
+ `bad-txns-legacy-asset-marker-after-nip040`, so "the node did not answer" must
718
+ not silently become "the node said rvn". The rejection propagates out of the
719
+ build — you never receive a partial result — and the node is queried only once,
720
+ whether the query succeeds or fails.
721
+
559
722
  ## Validations
560
723
 
561
724
  The library validates client-side:
@@ -744,11 +907,15 @@ are `xna` and `xna-test`; `xna-pq` and `xna-pq-test` remain available as compati
744
907
 
745
908
  ## Fee estimation (PQ-aware)
746
909
 
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.
910
+ 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
911
 
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`.
912
+ 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
913
 
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`.
914
+ 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.
915
+
916
+ 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`.
917
+
918
+ 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
919
 
753
920
  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
921
 
@@ -772,6 +939,12 @@ estimateInputVbytes({ address: 'nq1…' }); // 977
772
939
  estimateInputVbytes({ address: 'mgRYHdMq…' }); // 148
773
940
  estimateOutputBytes('tnq1…'); // 43
774
941
 
942
+ // Asset outputs declare their payload: kind is 'transfer' (default),
943
+ // 'owner', 'issue' or 'reissue'.
944
+ estimateOutputBytes({ address: 't7pv…', assetName: 'ROOTX' }); // 55
945
+ estimateOutputBytes({ address: 't7pv…', assetName: 'ROOTX!', kind: 'owner' }); // 48
946
+ estimateOutputBytes({ address: 't7pv…', assetName: 'ROOTX', kind: 'issue' }); // 58
947
+
775
948
  const vbytes = estimateTransactionVbytes(
776
949
  [{ script: '5120…' }, { address: 'mgRYHdMq…' }], // 1 PQ + 1 legacy input
777
950
  ['nq1qchange…', 'mgRYHdMqburn…'], // 1 PQ + 1 legacy output