@radiant-core/sdk 0.1.0 → 0.2.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/dist/index.d.cts CHANGED
@@ -223,6 +223,40 @@ declare function unpackRef(ref: string): {
223
223
  txid: string;
224
224
  vout: number;
225
225
  };
226
+ /** Owner address hash from a bare P2PKH script, if that's all it is. */
227
+ declare function parseP2pkhScript(scriptHex: string): {
228
+ addressHash?: string;
229
+ };
230
+ /**
231
+ * Parse an NFT output script into its singleton ref + owner hash.
232
+ *
233
+ * Matches the plain singleton:
234
+ * OP_PUSHINPUTREFSINGLETON <ref> OP_DROP <P2PKH>
235
+ * d8 <ref:72> 75 76a914 <h160:40> 88ac
236
+ *
237
+ * ...and the AUTH form that a mutable NFT (a WAVE name) is forced into after a
238
+ * target update — the mutable covenant makes the token output re-commit the
239
+ * mutable ref plus its scriptSig hash:
240
+ * ( OP_REQUIREINPUTREF <ref> <scriptSigHash> OP_2DROP )+ OP_STATESEPARATOR
241
+ * OP_PUSHINPUTREFSINGLETON <ref> OP_DROP <P2PKH>
242
+ *
243
+ * The singleton ref and owner are always the TRAILING `d8…/p2pkh`, so the
244
+ * optional preamble is skipped without changing what's captured. Returns {} for
245
+ * anything that isn't an NFT script.
246
+ */
247
+ declare function parseNftScript(scriptHex: string): {
248
+ ref?: string;
249
+ addressHash?: string;
250
+ };
251
+ /** Parse an FT output script into its ref + owner hash. Returns {} if not an FT. */
252
+ declare function parseFtScript(scriptHex: string): {
253
+ ref?: string;
254
+ addressHash?: string;
255
+ };
256
+ /** What kind of token a script is, if any. */
257
+ type TokenScriptKind = "nft" | "ft" | null;
258
+ /** Classify a token output script by SHAPE (not by a loose ref scan). */
259
+ declare function tokenScriptKind(scriptHex: string): TokenScriptKind;
226
260
 
227
261
  /**
228
262
  * ElectrumX WebSocket client.
@@ -631,15 +665,67 @@ interface TransferTokenParams {
631
665
  network?: NetworkName;
632
666
  }
633
667
  /**
634
- * Transfer a Glyph token (FT or NFT) to a new owner. The token output's value
635
- * is preserved (for FT this preserves the amount); the covenant preamble is
636
- * carried forward unchanged and only the owner P2PKH is re-bound.
668
+ * Transfer a Glyph token (FT or NFT, including a WAVE name) to a new owner.
669
+ *
670
+ * Moves the UTXO whole: the token output's value is preserved, which for an FT
671
+ * preserves the amount. To send PART of an FT balance (which needs multi-UTXO
672
+ * accumulation and a change output), that is a different operation and this is
673
+ * not it.
637
674
  */
638
675
  declare function transferToken(params: TransferTokenParams): Promise<{
639
676
  txid: string;
640
677
  hex: string;
641
678
  ref: string | null;
642
679
  }>;
680
+ interface TransferFungibleParams {
681
+ client: ElectrumClient;
682
+ /** Current owner address (signs the token inputs, pays the fee, gets change). */
683
+ address: string;
684
+ /** Current owner WIF. */
685
+ wif: string;
686
+ /**
687
+ * FT UTXOs to draw from. They must ALL be the same token — see the ref check
688
+ * in {@link transferFungible}. Pass the owner's whole balance for that ref;
689
+ * only as many as needed are spent.
690
+ */
691
+ tokenUtxos: Utxo[];
692
+ /** Recipient address. */
693
+ toAddress: string;
694
+ /**
695
+ * Amount to send, in the token's base units. For a Glyph FT the amount IS the
696
+ * output's photon value — the covenant sums photons per code-script.
697
+ */
698
+ amount: bigint;
699
+ /** Token-free RXD funding UTXOs for `address` to cover the fee. */
700
+ fundingUtxos: Utxo[];
701
+ feeRate?: bigint;
702
+ network?: NetworkName;
703
+ }
704
+ /**
705
+ * Send PART of a fungible balance, accumulating across UTXOs and returning the
706
+ * remainder as FT change.
707
+ *
708
+ * This is what {@link transferToken} cannot do: that moves one UTXO whole, so
709
+ * "send 50 of my 500" is inexpressible on it.
710
+ *
711
+ * The token outputs conserve the accumulated sum EXACTLY. That is not tidiness —
712
+ * the FT covenant enforces `sum(inputs) >= sum(outputs)` for its code-script
713
+ * (`OP_CODESCRIPTHASHVALUESUM_UTXOS ... OP_GREATERTHANOREQUAL OP_VERIFY`), so it
714
+ * permits burning and only forbids minting. Emit less than you spent and the
715
+ * difference is destroyed, silently and permanently. Hence the change output
716
+ * whenever the accumulation overshoots.
717
+ *
718
+ * The RXD arithmetic falls out of that conservation: token value in equals token
719
+ * value out, so the only surplus is the RXD funding, and `buildTx`'s change is
720
+ * exactly `funding - fee`.
721
+ */
722
+ declare function transferFungible(params: TransferFungibleParams): Promise<{
723
+ txid: string;
724
+ hex: string;
725
+ ref: string;
726
+ sent: bigint;
727
+ change: bigint;
728
+ }>;
643
729
 
644
730
  /**
645
731
  * WAVE name resolution.
@@ -678,6 +764,92 @@ declare function waveResolve(name: string, options?: WaveResolveOptions): Promis
678
764
  */
679
765
  declare function waveResolveAddress(name: string, options?: WaveResolveOptions): Promise<string | null>;
680
766
 
767
+ /**
768
+ * Token discovery — global, newest-first asset lists.
769
+ *
770
+ * Backed by the RXinDexer v4 discovery indexes (Glyph DB schema 4,
771
+ * deployed 2026-07-18):
772
+ * GET {base}/glyphs/recent — newest across ALL types
773
+ * GET {base}/glyphs/by-type/{typeId}?order=… — one type, ref or recent order
774
+ *
775
+ * Pages are cursor-paginated: pass the previous response's `nextCursor` back
776
+ * to fetch the next page. Cursors are opaque, URL-safe strings, and are
777
+ * order-specific — never reuse a cursor across a change of `order` or type.
778
+ *
779
+ * The killer pattern this enables is an incremental watermark sync: do one
780
+ * full walk, remember the newest `deploy_height` you saw, then on later runs
781
+ * page newest-first and STOP as soon as `deploy_height` drops below your
782
+ * watermark — you only ever pay for tokens minted since the last run.
783
+ */
784
+ /** GlyphTokenType ids as served by the indexer (`type` field). */
785
+ declare const GLYPH_TOKEN_TYPE: {
786
+ readonly UNKNOWN: 0;
787
+ readonly FT: 1;
788
+ readonly NFT: 2;
789
+ readonly DAT: 3;
790
+ readonly DMINT: 4;
791
+ readonly WAVE: 5;
792
+ readonly CONTAINER: 6;
793
+ readonly AUTHORITY: 7;
794
+ };
795
+ type GlyphTokenTypeId = (typeof GLYPH_TOKEN_TYPE)[keyof typeof GLYPH_TOKEN_TYPE];
796
+ /**
797
+ * One token row from a discovery list. Only the stable, always-present core is
798
+ * typed; the indexer returns many more fields (supply, dMint, icon, attrs, …)
799
+ * which remain reachable through the index signature.
800
+ */
801
+ interface GlyphTokenSummary {
802
+ /** Display ref: big-endian txid + "_" + vout (canonical output form). */
803
+ ref: string;
804
+ /** 72-hex internal ref (raw index key bytes) — feed this to get_by_ref-style APIs. */
805
+ ref_hex: string;
806
+ /** Primary token type id (see GLYPH_TOKEN_TYPE). */
807
+ type: number;
808
+ type_name: string;
809
+ /** Glyph protocol ids the token carries (1=FT, 2=NFT, 8=ENCRYPTED, 11=WAVE, …). */
810
+ protocols: number[];
811
+ name: string | null;
812
+ ticker: string | null;
813
+ deploy_height: number;
814
+ deploy_txid: string;
815
+ is_spent: boolean;
816
+ [extra: string]: unknown;
817
+ }
818
+ /** One page of a discovery list. */
819
+ interface TokenPage {
820
+ tokens: GlyphTokenSummary[];
821
+ /** Pass back as `cursor` to fetch the next page; null = no more pages. */
822
+ nextCursor: string | null;
823
+ }
824
+ /** Options shared by the discovery queries. */
825
+ interface DiscoveryOptions {
826
+ /** Max rows per page (indexer caps at 500). Default 100. */
827
+ limit?: number;
828
+ /** Opaque cursor from the previous page's `nextCursor`. */
829
+ cursor?: string;
830
+ /** REST base URL. Default https://radiantcore.org/api */
831
+ restBase?: string;
832
+ /** Inject a fetch implementation (tests / non-global-fetch runtimes). */
833
+ fetchImpl?: typeof fetch;
834
+ /** AbortSignal to cancel the request. */
835
+ signal?: AbortSignal;
836
+ }
837
+ /**
838
+ * Newest-deployed tokens across every type (or one type via `typeId`),
839
+ * newest-first.
840
+ */
841
+ declare function getRecentTokens(options?: DiscoveryOptions & {
842
+ typeId?: GlyphTokenTypeId | number;
843
+ }): Promise<TokenPage>;
844
+ /**
845
+ * Tokens of one type. `order: "recent"` = newest-deployed first;
846
+ * `order: "ref"` (default, matches the pre-v4 API) = stable ref-hash order.
847
+ * Cursors are order-specific.
848
+ */
849
+ declare function getTokensByType(typeId: GlyphTokenTypeId | number, options?: DiscoveryOptions & {
850
+ order?: "ref" | "recent";
851
+ }): Promise<TokenPage>;
852
+
681
853
  /**
682
854
  * Single, centralised access point to `@radiant-core/radiantjs`.
683
855
  *
@@ -690,4 +862,4 @@ declare function waveResolveAddress(name: string, options?: WaveResolveOptions):
690
862
  /** The raw radiantjs namespace, for advanced callers that need an escape hatch. */
691
863
  declare const radiantjs: any;
692
864
 
693
- export { type BuildTxInput, type BuildTxParams, type BuiltTx, DEFAULT_ELECTRUM_ENDPOINT, DEFAULT_REST_BASE, DUST_LIMIT, type DerivedKey, ElectrumClient, type ElectrumClientOptions, ElectrumError, type FundingSelection, GLYPH_PROTOCOL, type GlyphPayload, HDWallet, type HDWalletOptions, InsufficientFundsError, Keys, MIN_RELAY_FEE_RATE, type MintFtParams, type MintNftParams, type MintResult, type NetworkName, PHOTONS_PER_RXD, RADIANT_COIN_TYPE, RXD_DECIMALS, RadiantSdkError, type ScriptHashBalance, type SelectRxdFundingOptions, TokenBurnGuardError, type TransferTokenParams, type TxOutput, type Utxo, type UtxoRef, ValidationError, type WaveResolution, type WaveResolveOptions, addressToScriptHash, assertFundingSafe, buildRxdTransfer, buildTx, encodeGlyph, estimateFee, filterFundingCandidates, ftScript, isFundingSafe, isTokenBearing, mintFT, mintNFT, nftScript, p2pkhScript, packRef, parseTokenRef, photonsToRxd, radiantjs, rxdToPhotons, scriptHash, selectRxdFunding, sumValue, transferToken, unpackRef, waveLabel, waveResolve, waveResolveAddress, zeroRefs };
865
+ export { type BuildTxInput, type BuildTxParams, type BuiltTx, DEFAULT_ELECTRUM_ENDPOINT, DEFAULT_REST_BASE, DUST_LIMIT, type DerivedKey, type DiscoveryOptions, ElectrumClient, type ElectrumClientOptions, ElectrumError, type FundingSelection, GLYPH_PROTOCOL, GLYPH_TOKEN_TYPE, type GlyphPayload, type GlyphTokenSummary, type GlyphTokenTypeId, HDWallet, type HDWalletOptions, InsufficientFundsError, Keys, MIN_RELAY_FEE_RATE, type MintFtParams, type MintNftParams, type MintResult, type NetworkName, PHOTONS_PER_RXD, RADIANT_COIN_TYPE, RXD_DECIMALS, RadiantSdkError, type ScriptHashBalance, type SelectRxdFundingOptions, TokenBurnGuardError, type TokenPage, type TokenScriptKind, type TransferFungibleParams, type TransferTokenParams, type TxOutput, type Utxo, type UtxoRef, ValidationError, type WaveResolution, type WaveResolveOptions, addressToScriptHash, assertFundingSafe, buildRxdTransfer, buildTx, encodeGlyph, estimateFee, filterFundingCandidates, ftScript, getRecentTokens, getTokensByType, isFundingSafe, isTokenBearing, mintFT, mintNFT, nftScript, p2pkhScript, packRef, parseFtScript, parseNftScript, parseP2pkhScript, parseTokenRef, photonsToRxd, radiantjs, rxdToPhotons, scriptHash, selectRxdFunding, sumValue, tokenScriptKind, transferFungible, transferToken, unpackRef, waveLabel, waveResolve, waveResolveAddress, zeroRefs };
package/dist/index.d.ts CHANGED
@@ -223,6 +223,40 @@ declare function unpackRef(ref: string): {
223
223
  txid: string;
224
224
  vout: number;
225
225
  };
226
+ /** Owner address hash from a bare P2PKH script, if that's all it is. */
227
+ declare function parseP2pkhScript(scriptHex: string): {
228
+ addressHash?: string;
229
+ };
230
+ /**
231
+ * Parse an NFT output script into its singleton ref + owner hash.
232
+ *
233
+ * Matches the plain singleton:
234
+ * OP_PUSHINPUTREFSINGLETON <ref> OP_DROP <P2PKH>
235
+ * d8 <ref:72> 75 76a914 <h160:40> 88ac
236
+ *
237
+ * ...and the AUTH form that a mutable NFT (a WAVE name) is forced into after a
238
+ * target update — the mutable covenant makes the token output re-commit the
239
+ * mutable ref plus its scriptSig hash:
240
+ * ( OP_REQUIREINPUTREF <ref> <scriptSigHash> OP_2DROP )+ OP_STATESEPARATOR
241
+ * OP_PUSHINPUTREFSINGLETON <ref> OP_DROP <P2PKH>
242
+ *
243
+ * The singleton ref and owner are always the TRAILING `d8…/p2pkh`, so the
244
+ * optional preamble is skipped without changing what's captured. Returns {} for
245
+ * anything that isn't an NFT script.
246
+ */
247
+ declare function parseNftScript(scriptHex: string): {
248
+ ref?: string;
249
+ addressHash?: string;
250
+ };
251
+ /** Parse an FT output script into its ref + owner hash. Returns {} if not an FT. */
252
+ declare function parseFtScript(scriptHex: string): {
253
+ ref?: string;
254
+ addressHash?: string;
255
+ };
256
+ /** What kind of token a script is, if any. */
257
+ type TokenScriptKind = "nft" | "ft" | null;
258
+ /** Classify a token output script by SHAPE (not by a loose ref scan). */
259
+ declare function tokenScriptKind(scriptHex: string): TokenScriptKind;
226
260
 
227
261
  /**
228
262
  * ElectrumX WebSocket client.
@@ -631,15 +665,67 @@ interface TransferTokenParams {
631
665
  network?: NetworkName;
632
666
  }
633
667
  /**
634
- * Transfer a Glyph token (FT or NFT) to a new owner. The token output's value
635
- * is preserved (for FT this preserves the amount); the covenant preamble is
636
- * carried forward unchanged and only the owner P2PKH is re-bound.
668
+ * Transfer a Glyph token (FT or NFT, including a WAVE name) to a new owner.
669
+ *
670
+ * Moves the UTXO whole: the token output's value is preserved, which for an FT
671
+ * preserves the amount. To send PART of an FT balance (which needs multi-UTXO
672
+ * accumulation and a change output), that is a different operation and this is
673
+ * not it.
637
674
  */
638
675
  declare function transferToken(params: TransferTokenParams): Promise<{
639
676
  txid: string;
640
677
  hex: string;
641
678
  ref: string | null;
642
679
  }>;
680
+ interface TransferFungibleParams {
681
+ client: ElectrumClient;
682
+ /** Current owner address (signs the token inputs, pays the fee, gets change). */
683
+ address: string;
684
+ /** Current owner WIF. */
685
+ wif: string;
686
+ /**
687
+ * FT UTXOs to draw from. They must ALL be the same token — see the ref check
688
+ * in {@link transferFungible}. Pass the owner's whole balance for that ref;
689
+ * only as many as needed are spent.
690
+ */
691
+ tokenUtxos: Utxo[];
692
+ /** Recipient address. */
693
+ toAddress: string;
694
+ /**
695
+ * Amount to send, in the token's base units. For a Glyph FT the amount IS the
696
+ * output's photon value — the covenant sums photons per code-script.
697
+ */
698
+ amount: bigint;
699
+ /** Token-free RXD funding UTXOs for `address` to cover the fee. */
700
+ fundingUtxos: Utxo[];
701
+ feeRate?: bigint;
702
+ network?: NetworkName;
703
+ }
704
+ /**
705
+ * Send PART of a fungible balance, accumulating across UTXOs and returning the
706
+ * remainder as FT change.
707
+ *
708
+ * This is what {@link transferToken} cannot do: that moves one UTXO whole, so
709
+ * "send 50 of my 500" is inexpressible on it.
710
+ *
711
+ * The token outputs conserve the accumulated sum EXACTLY. That is not tidiness —
712
+ * the FT covenant enforces `sum(inputs) >= sum(outputs)` for its code-script
713
+ * (`OP_CODESCRIPTHASHVALUESUM_UTXOS ... OP_GREATERTHANOREQUAL OP_VERIFY`), so it
714
+ * permits burning and only forbids minting. Emit less than you spent and the
715
+ * difference is destroyed, silently and permanently. Hence the change output
716
+ * whenever the accumulation overshoots.
717
+ *
718
+ * The RXD arithmetic falls out of that conservation: token value in equals token
719
+ * value out, so the only surplus is the RXD funding, and `buildTx`'s change is
720
+ * exactly `funding - fee`.
721
+ */
722
+ declare function transferFungible(params: TransferFungibleParams): Promise<{
723
+ txid: string;
724
+ hex: string;
725
+ ref: string;
726
+ sent: bigint;
727
+ change: bigint;
728
+ }>;
643
729
 
644
730
  /**
645
731
  * WAVE name resolution.
@@ -678,6 +764,92 @@ declare function waveResolve(name: string, options?: WaveResolveOptions): Promis
678
764
  */
679
765
  declare function waveResolveAddress(name: string, options?: WaveResolveOptions): Promise<string | null>;
680
766
 
767
+ /**
768
+ * Token discovery — global, newest-first asset lists.
769
+ *
770
+ * Backed by the RXinDexer v4 discovery indexes (Glyph DB schema 4,
771
+ * deployed 2026-07-18):
772
+ * GET {base}/glyphs/recent — newest across ALL types
773
+ * GET {base}/glyphs/by-type/{typeId}?order=… — one type, ref or recent order
774
+ *
775
+ * Pages are cursor-paginated: pass the previous response's `nextCursor` back
776
+ * to fetch the next page. Cursors are opaque, URL-safe strings, and are
777
+ * order-specific — never reuse a cursor across a change of `order` or type.
778
+ *
779
+ * The killer pattern this enables is an incremental watermark sync: do one
780
+ * full walk, remember the newest `deploy_height` you saw, then on later runs
781
+ * page newest-first and STOP as soon as `deploy_height` drops below your
782
+ * watermark — you only ever pay for tokens minted since the last run.
783
+ */
784
+ /** GlyphTokenType ids as served by the indexer (`type` field). */
785
+ declare const GLYPH_TOKEN_TYPE: {
786
+ readonly UNKNOWN: 0;
787
+ readonly FT: 1;
788
+ readonly NFT: 2;
789
+ readonly DAT: 3;
790
+ readonly DMINT: 4;
791
+ readonly WAVE: 5;
792
+ readonly CONTAINER: 6;
793
+ readonly AUTHORITY: 7;
794
+ };
795
+ type GlyphTokenTypeId = (typeof GLYPH_TOKEN_TYPE)[keyof typeof GLYPH_TOKEN_TYPE];
796
+ /**
797
+ * One token row from a discovery list. Only the stable, always-present core is
798
+ * typed; the indexer returns many more fields (supply, dMint, icon, attrs, …)
799
+ * which remain reachable through the index signature.
800
+ */
801
+ interface GlyphTokenSummary {
802
+ /** Display ref: big-endian txid + "_" + vout (canonical output form). */
803
+ ref: string;
804
+ /** 72-hex internal ref (raw index key bytes) — feed this to get_by_ref-style APIs. */
805
+ ref_hex: string;
806
+ /** Primary token type id (see GLYPH_TOKEN_TYPE). */
807
+ type: number;
808
+ type_name: string;
809
+ /** Glyph protocol ids the token carries (1=FT, 2=NFT, 8=ENCRYPTED, 11=WAVE, …). */
810
+ protocols: number[];
811
+ name: string | null;
812
+ ticker: string | null;
813
+ deploy_height: number;
814
+ deploy_txid: string;
815
+ is_spent: boolean;
816
+ [extra: string]: unknown;
817
+ }
818
+ /** One page of a discovery list. */
819
+ interface TokenPage {
820
+ tokens: GlyphTokenSummary[];
821
+ /** Pass back as `cursor` to fetch the next page; null = no more pages. */
822
+ nextCursor: string | null;
823
+ }
824
+ /** Options shared by the discovery queries. */
825
+ interface DiscoveryOptions {
826
+ /** Max rows per page (indexer caps at 500). Default 100. */
827
+ limit?: number;
828
+ /** Opaque cursor from the previous page's `nextCursor`. */
829
+ cursor?: string;
830
+ /** REST base URL. Default https://radiantcore.org/api */
831
+ restBase?: string;
832
+ /** Inject a fetch implementation (tests / non-global-fetch runtimes). */
833
+ fetchImpl?: typeof fetch;
834
+ /** AbortSignal to cancel the request. */
835
+ signal?: AbortSignal;
836
+ }
837
+ /**
838
+ * Newest-deployed tokens across every type (or one type via `typeId`),
839
+ * newest-first.
840
+ */
841
+ declare function getRecentTokens(options?: DiscoveryOptions & {
842
+ typeId?: GlyphTokenTypeId | number;
843
+ }): Promise<TokenPage>;
844
+ /**
845
+ * Tokens of one type. `order: "recent"` = newest-deployed first;
846
+ * `order: "ref"` (default, matches the pre-v4 API) = stable ref-hash order.
847
+ * Cursors are order-specific.
848
+ */
849
+ declare function getTokensByType(typeId: GlyphTokenTypeId | number, options?: DiscoveryOptions & {
850
+ order?: "ref" | "recent";
851
+ }): Promise<TokenPage>;
852
+
681
853
  /**
682
854
  * Single, centralised access point to `@radiant-core/radiantjs`.
683
855
  *
@@ -690,4 +862,4 @@ declare function waveResolveAddress(name: string, options?: WaveResolveOptions):
690
862
  /** The raw radiantjs namespace, for advanced callers that need an escape hatch. */
691
863
  declare const radiantjs: any;
692
864
 
693
- export { type BuildTxInput, type BuildTxParams, type BuiltTx, DEFAULT_ELECTRUM_ENDPOINT, DEFAULT_REST_BASE, DUST_LIMIT, type DerivedKey, ElectrumClient, type ElectrumClientOptions, ElectrumError, type FundingSelection, GLYPH_PROTOCOL, type GlyphPayload, HDWallet, type HDWalletOptions, InsufficientFundsError, Keys, MIN_RELAY_FEE_RATE, type MintFtParams, type MintNftParams, type MintResult, type NetworkName, PHOTONS_PER_RXD, RADIANT_COIN_TYPE, RXD_DECIMALS, RadiantSdkError, type ScriptHashBalance, type SelectRxdFundingOptions, TokenBurnGuardError, type TransferTokenParams, type TxOutput, type Utxo, type UtxoRef, ValidationError, type WaveResolution, type WaveResolveOptions, addressToScriptHash, assertFundingSafe, buildRxdTransfer, buildTx, encodeGlyph, estimateFee, filterFundingCandidates, ftScript, isFundingSafe, isTokenBearing, mintFT, mintNFT, nftScript, p2pkhScript, packRef, parseTokenRef, photonsToRxd, radiantjs, rxdToPhotons, scriptHash, selectRxdFunding, sumValue, transferToken, unpackRef, waveLabel, waveResolve, waveResolveAddress, zeroRefs };
865
+ export { type BuildTxInput, type BuildTxParams, type BuiltTx, DEFAULT_ELECTRUM_ENDPOINT, DEFAULT_REST_BASE, DUST_LIMIT, type DerivedKey, type DiscoveryOptions, ElectrumClient, type ElectrumClientOptions, ElectrumError, type FundingSelection, GLYPH_PROTOCOL, GLYPH_TOKEN_TYPE, type GlyphPayload, type GlyphTokenSummary, type GlyphTokenTypeId, HDWallet, type HDWalletOptions, InsufficientFundsError, Keys, MIN_RELAY_FEE_RATE, type MintFtParams, type MintNftParams, type MintResult, type NetworkName, PHOTONS_PER_RXD, RADIANT_COIN_TYPE, RXD_DECIMALS, RadiantSdkError, type ScriptHashBalance, type SelectRxdFundingOptions, TokenBurnGuardError, type TokenPage, type TokenScriptKind, type TransferFungibleParams, type TransferTokenParams, type TxOutput, type Utxo, type UtxoRef, ValidationError, type WaveResolution, type WaveResolveOptions, addressToScriptHash, assertFundingSafe, buildRxdTransfer, buildTx, encodeGlyph, estimateFee, filterFundingCandidates, ftScript, getRecentTokens, getTokensByType, isFundingSafe, isTokenBearing, mintFT, mintNFT, nftScript, p2pkhScript, packRef, parseFtScript, parseNftScript, parseP2pkhScript, parseTokenRef, photonsToRxd, radiantjs, rxdToPhotons, scriptHash, selectRxdFunding, sumValue, tokenScriptKind, transferFungible, transferToken, unpackRef, waveLabel, waveResolve, waveResolveAddress, zeroRefs };
package/dist/index.js CHANGED
@@ -43,6 +43,11 @@ var MIN_RELAY_FEE_RATE = {
43
43
  testnet: 1000n,
44
44
  regtest: 1000n
45
45
  };
46
+ var MAX_REASONABLE_FEE_RATE = {
47
+ mainnet: MIN_RELAY_FEE_RATE.mainnet * 2n,
48
+ testnet: MIN_RELAY_FEE_RATE.testnet * 2n,
49
+ regtest: MIN_RELAY_FEE_RATE.regtest * 2n
50
+ };
46
51
  var DUST_LIMIT = 1000n;
47
52
  var TOKEN_OUTPUT_VALUE = 1n;
48
53
  var DEFAULT_ELECTRUM_ENDPOINT = {
@@ -241,6 +246,25 @@ function unpackRef(ref) {
241
246
  const vout = buf.readUInt32LE(32);
242
247
  return { txid, vout };
243
248
  }
249
+ function parseP2pkhScript(scriptHex) {
250
+ const [, addressHash] = scriptHex.toLowerCase().match(/^76a914([0-9a-f]{40})88ac$/) || [];
251
+ return { addressHash };
252
+ }
253
+ function parseNftScript(scriptHex) {
254
+ const pattern = /^(?:d1[0-9a-f]{72}20[0-9a-f]{64}6d)*(?:bd)?d8([0-9a-f]{72})7576a914([0-9a-f]{40})88ac$/;
255
+ const [, ref, addressHash] = scriptHex.toLowerCase().match(pattern) || [];
256
+ return { ref, addressHash };
257
+ }
258
+ function parseFtScript(scriptHex) {
259
+ const pattern = /^76a914([0-9a-f]{40})88acbdd0([0-9a-f]{72})dec0e9aa76e378e4a269e69d$/;
260
+ const [, addressHash, ref] = scriptHex.toLowerCase().match(pattern) || [];
261
+ return { ref, addressHash };
262
+ }
263
+ function tokenScriptKind(scriptHex) {
264
+ if (parseNftScript(scriptHex).ref) return "nft";
265
+ if (parseFtScript(scriptHex).ref) return "ft";
266
+ return null;
267
+ }
244
268
 
245
269
  // src/client.ts
246
270
  var WS_OPEN = 1;
@@ -646,6 +670,17 @@ function sumValue(utxos) {
646
670
  }
647
671
 
648
672
  // src/tx.ts
673
+ function assertSaneFee(tx, network) {
674
+ const sizeBytes = BigInt(tx.toString().length / 2);
675
+ const reference = MAX_REASONABLE_FEE_RATE[network];
676
+ const ceiling = sizeBytes * reference * 12n / 10n;
677
+ const actual = BigInt(tx.getFee?.() ?? 0);
678
+ if (actual > ceiling) {
679
+ throw new ValidationError(
680
+ `buildTx: fee ${actual} photons is above the sanity ceiling ${ceiling} for a ${sizeBytes}-byte tx (${network} reference ${reference} photons/byte). Refusing to build \u2014 this is almost always a units mistake or an over-funded change-less tx, and the excess would be paid to miners irrecoverably.`
681
+ );
682
+ }
683
+ }
649
684
  function bn(value) {
650
685
  return new crypto.BN(value.toString());
651
686
  }
@@ -720,6 +755,7 @@ function buildTx(params) {
720
755
  }
721
756
  tx.sign(privKeys[0]);
722
757
  if (typeof tx.seal === "function") tx.seal();
758
+ assertSaneFee(tx, network);
723
759
  const hex = tx.toString();
724
760
  const resolvedOutputs = tx.outputs.map((o) => ({
725
761
  script: o.script.toHex(),
@@ -803,16 +839,14 @@ function parseTokenRef(scriptHex) {
803
839
  }
804
840
  return null;
805
841
  }
806
- function rebindOwner(scriptHex, toAddress, network) {
807
- const newPkh = Address.fromString(toAddress).hashBuffer.toString("hex");
808
- const re = /76a914[0-9a-f]{40}88ac/i;
809
- const matches = scriptHex.match(new RegExp(re, "gi"));
810
- if (!matches || matches.length !== 1) {
811
- throw new ValidationError(
812
- "rebindOwner: expected exactly one owner P2PKH in the token script"
813
- );
814
- }
815
- return scriptHex.replace(re, `76a914${newPkh}88ac`);
842
+ function tokenTransferScript(scriptHex, toAddress, network) {
843
+ const nft = parseNftScript(scriptHex);
844
+ if (nft.ref) return nftScript(toAddress, nft.ref, network);
845
+ const ft = parseFtScript(scriptHex);
846
+ if (ft.ref) return ftScript(toAddress, ft.ref, network);
847
+ throw new ValidationError(
848
+ "transferToken: tokenUtxo.script is not a recognised NFT or FT output script"
849
+ );
816
850
  }
817
851
  async function commitReveal(base, payload, refType, buildRevealOutputScript, tokenOutputValue) {
818
852
  const network = base.network ?? "mainnet";
@@ -926,8 +960,8 @@ async function transferToken(params) {
926
960
  "transferToken: tokenUtxo.script is required to rebuild the token output"
927
961
  );
928
962
  }
929
- const newScript = rebindOwner(tokenUtxo.script, params.toAddress);
930
- const ref = parseTokenRef(tokenUtxo.script);
963
+ const newScript = tokenTransferScript(tokenUtxo.script, params.toAddress, network);
964
+ const ref = parseNftScript(tokenUtxo.script).ref ?? parseFtScript(tokenUtxo.script).ref ?? null;
931
965
  const selection = selectRxdFunding(params.fundingUtxos, 0n, feeRate, {
932
966
  baseOutputCount: 1,
933
967
  // the token output
@@ -961,6 +995,76 @@ async function transferToken(params) {
961
995
  await params.client.broadcastTx(built.hex);
962
996
  return { txid: built.txid, hex: built.hex, ref };
963
997
  }
998
+ async function transferFungible(params) {
999
+ const network = params.network ?? "mainnet";
1000
+ const feeRate = params.feeRate ?? MIN_RELAY_FEE_RATE[network];
1001
+ const { amount, tokenUtxos } = params;
1002
+ if (amount <= 0n) {
1003
+ throw new ValidationError("transferFungible: amount must be positive");
1004
+ }
1005
+ if (!tokenUtxos.length) {
1006
+ throw new ValidationError("transferFungible: no token UTXOs given");
1007
+ }
1008
+ let ref;
1009
+ for (const u of tokenUtxos) {
1010
+ const parsed = u.script ? parseFtScript(u.script) : {};
1011
+ if (!parsed.ref) {
1012
+ throw new ValidationError(
1013
+ `transferFungible: ${u.txid}:${u.vout} is not an FT output (its script must be the on-chain ftScript)`
1014
+ );
1015
+ }
1016
+ if (ref && parsed.ref !== ref) {
1017
+ throw new ValidationError(
1018
+ "transferFungible: token UTXOs are for different tokens \u2014 refusing to mix refs, it would burn one of them"
1019
+ );
1020
+ }
1021
+ ref = parsed.ref;
1022
+ }
1023
+ const sorted = [...tokenUtxos].sort((a, b) => a.value < b.value ? 1 : a.value > b.value ? -1 : 0);
1024
+ const spend = [];
1025
+ let sum = 0n;
1026
+ for (const u of sorted) {
1027
+ spend.push(u);
1028
+ sum += u.value;
1029
+ if (sum >= amount) break;
1030
+ }
1031
+ if (sum < amount) {
1032
+ throw new ValidationError(
1033
+ `transferFungible: insufficient token balance \u2014 have ${sum}, need ${amount}`
1034
+ );
1035
+ }
1036
+ const change = sum - amount;
1037
+ const outputs = [{ script: ftScript(params.toAddress, ref, network), value: amount }];
1038
+ if (change > 0n) {
1039
+ outputs.push({ script: ftScript(params.address, ref, network), value: change });
1040
+ }
1041
+ const selection = selectRxdFunding(params.fundingUtxos, 0n, feeRate, {
1042
+ baseOutputCount: outputs.length,
1043
+ extraInputBytes: BigInt(148 * spend.length),
1044
+ // the token inputs we add below
1045
+ withChange: true
1046
+ });
1047
+ const inputs = [
1048
+ ...spend.map((u) => ({ txid: u.txid, vout: u.vout, value: u.value, script: u.script })),
1049
+ ...selection.inputs.map((u) => ({
1050
+ txid: u.txid,
1051
+ vout: u.vout,
1052
+ value: u.value,
1053
+ script: u.script || void 0
1054
+ }))
1055
+ ];
1056
+ const built = buildTx({
1057
+ address: params.address,
1058
+ wif: params.wif,
1059
+ inputs,
1060
+ outputs,
1061
+ addChange: true,
1062
+ feeRate,
1063
+ network
1064
+ });
1065
+ await params.client.broadcastTx(built.hex);
1066
+ return { txid: built.txid, hex: built.hex, ref, sent: amount, change };
1067
+ }
964
1068
 
965
1069
  // src/wave.ts
966
1070
  function waveLabel(name) {
@@ -1010,6 +1114,61 @@ async function waveResolveAddress(name, options = {}) {
1010
1114
  return r.registered ? r.address ?? null : null;
1011
1115
  }
1012
1116
 
1013
- export { DEFAULT_ELECTRUM_ENDPOINT, DEFAULT_REST_BASE, DUST_LIMIT, ElectrumClient, ElectrumError, GLYPH_PROTOCOL, HDWallet, InsufficientFundsError, Keys, MIN_RELAY_FEE_RATE, PHOTONS_PER_RXD, RADIANT_COIN_TYPE, RXD_DECIMALS, RadiantSdkError, TokenBurnGuardError, ValidationError, addressToScriptHash, assertFundingSafe, buildRxdTransfer, buildTx, encodeGlyph, estimateFee, filterFundingCandidates, ftScript, isFundingSafe, isTokenBearing, mintFT, mintNFT, nftScript, p2pkhScript, packRef, parseTokenRef, photonsToRxd, radiantjs, rxdToPhotons, scriptHash, selectRxdFunding, sumValue, transferToken, unpackRef, waveLabel, waveResolve, waveResolveAddress, zeroRefs };
1117
+ // src/discovery.ts
1118
+ var GLYPH_TOKEN_TYPE = {
1119
+ UNKNOWN: 0,
1120
+ FT: 1,
1121
+ NFT: 2,
1122
+ DAT: 3,
1123
+ DMINT: 4,
1124
+ WAVE: 5,
1125
+ CONTAINER: 6,
1126
+ AUTHORITY: 7
1127
+ };
1128
+ async function fetchTokenPage(path, params, options, fnName) {
1129
+ const base = (options.restBase ?? DEFAULT_REST_BASE).replace(/\/+$/, "");
1130
+ const doFetch = options.fetchImpl ?? globalThis.fetch;
1131
+ if (typeof doFetch !== "function") {
1132
+ throw new ValidationError(
1133
+ `${fnName}: no fetch implementation available; pass options.fetchImpl`
1134
+ );
1135
+ }
1136
+ const query = new URLSearchParams();
1137
+ for (const [k, v] of Object.entries(params)) {
1138
+ if (v !== void 0) query.set(k, String(v));
1139
+ }
1140
+ const qs = query.toString();
1141
+ const url = `${base}${path}${qs ? `?${qs}` : ""}`;
1142
+ const res = await doFetch(url, { signal: options.signal });
1143
+ if (!res.ok) {
1144
+ throw new ValidationError(`${fnName}: HTTP ${res.status} ${res.statusText}`);
1145
+ }
1146
+ const data = await res.json();
1147
+ return {
1148
+ tokens: Array.isArray(data?.tokens) ? data.tokens : [],
1149
+ nextCursor: data?.next_cursor ?? null
1150
+ };
1151
+ }
1152
+ async function getRecentTokens(options = {}) {
1153
+ return fetchTokenPage(
1154
+ "/glyphs/recent",
1155
+ { limit: options.limit, cursor: options.cursor, type_id: options.typeId },
1156
+ options,
1157
+ "getRecentTokens"
1158
+ );
1159
+ }
1160
+ async function getTokensByType(typeId, options = {}) {
1161
+ if (!Number.isInteger(typeId) || typeId < 0 || typeId > 7) {
1162
+ throw new ValidationError(`getTokensByType: invalid typeId ${typeId}`);
1163
+ }
1164
+ return fetchTokenPage(
1165
+ `/glyphs/by-type/${typeId}`,
1166
+ { limit: options.limit, cursor: options.cursor, order: options.order },
1167
+ options,
1168
+ "getTokensByType"
1169
+ );
1170
+ }
1171
+
1172
+ export { DEFAULT_ELECTRUM_ENDPOINT, DEFAULT_REST_BASE, DUST_LIMIT, ElectrumClient, ElectrumError, GLYPH_PROTOCOL, GLYPH_TOKEN_TYPE, HDWallet, InsufficientFundsError, Keys, MIN_RELAY_FEE_RATE, PHOTONS_PER_RXD, RADIANT_COIN_TYPE, RXD_DECIMALS, RadiantSdkError, TokenBurnGuardError, ValidationError, addressToScriptHash, assertFundingSafe, buildRxdTransfer, buildTx, encodeGlyph, estimateFee, filterFundingCandidates, ftScript, getRecentTokens, getTokensByType, isFundingSafe, isTokenBearing, mintFT, mintNFT, nftScript, p2pkhScript, packRef, parseFtScript, parseNftScript, parseP2pkhScript, parseTokenRef, photonsToRxd, radiantjs, rxdToPhotons, scriptHash, selectRxdFunding, sumValue, tokenScriptKind, transferFungible, transferToken, unpackRef, waveLabel, waveResolve, waveResolveAddress, zeroRefs };
1014
1173
  //# sourceMappingURL=index.js.map
1015
1174
  //# sourceMappingURL=index.js.map