@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/README.md CHANGED
@@ -186,6 +186,18 @@ const status = await client.subscribe(me.address, (newStatus) => {
186
186
  | Units | `rxdToPhotons`, `photonsToRxd` |
187
187
  | Errors | `RadiantSdkError`, `InsufficientFundsError`, `TokenBurnGuardError`, `ElectrumError`, `ValidationError` |
188
188
 
189
+ See **[docs/API.md](docs/API.md)** for the full reference (every export, options,
190
+ and return types), and the hosted guide at
191
+ **[radiantcore.org/docs/sdk.html](https://radiantcore.org/docs/sdk.html)**.
192
+
193
+ ---
194
+
195
+ ## Documentation
196
+
197
+ - **[API reference](docs/API.md)** — every export, with options and return types.
198
+ - **[Hosted guide](https://radiantcore.org/docs/sdk.html)** — narrative walkthrough on radiantcore.org.
199
+ - **[Changelog](CHANGELOG.md)** · **[Contributing](CONTRIBUTING.md)**
200
+
189
201
  ---
190
202
 
191
203
  ## Design notes
package/dist/index.cjs CHANGED
@@ -49,6 +49,11 @@ var MIN_RELAY_FEE_RATE = {
49
49
  testnet: 1000n,
50
50
  regtest: 1000n
51
51
  };
52
+ var MAX_REASONABLE_FEE_RATE = {
53
+ mainnet: MIN_RELAY_FEE_RATE.mainnet * 2n,
54
+ testnet: MIN_RELAY_FEE_RATE.testnet * 2n,
55
+ regtest: MIN_RELAY_FEE_RATE.regtest * 2n
56
+ };
52
57
  var DUST_LIMIT = 1000n;
53
58
  var TOKEN_OUTPUT_VALUE = 1n;
54
59
  var DEFAULT_ELECTRUM_ENDPOINT = {
@@ -247,6 +252,25 @@ function unpackRef(ref) {
247
252
  const vout = buf.readUInt32LE(32);
248
253
  return { txid, vout };
249
254
  }
255
+ function parseP2pkhScript(scriptHex) {
256
+ const [, addressHash] = scriptHex.toLowerCase().match(/^76a914([0-9a-f]{40})88ac$/) || [];
257
+ return { addressHash };
258
+ }
259
+ function parseNftScript(scriptHex) {
260
+ 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$/;
261
+ const [, ref, addressHash] = scriptHex.toLowerCase().match(pattern) || [];
262
+ return { ref, addressHash };
263
+ }
264
+ function parseFtScript(scriptHex) {
265
+ const pattern = /^76a914([0-9a-f]{40})88acbdd0([0-9a-f]{72})dec0e9aa76e378e4a269e69d$/;
266
+ const [, addressHash, ref] = scriptHex.toLowerCase().match(pattern) || [];
267
+ return { ref, addressHash };
268
+ }
269
+ function tokenScriptKind(scriptHex) {
270
+ if (parseNftScript(scriptHex).ref) return "nft";
271
+ if (parseFtScript(scriptHex).ref) return "ft";
272
+ return null;
273
+ }
250
274
 
251
275
  // src/client.ts
252
276
  var WS_OPEN = 1;
@@ -652,6 +676,17 @@ function sumValue(utxos) {
652
676
  }
653
677
 
654
678
  // src/tx.ts
679
+ function assertSaneFee(tx, network) {
680
+ const sizeBytes = BigInt(tx.toString().length / 2);
681
+ const reference = MAX_REASONABLE_FEE_RATE[network];
682
+ const ceiling = sizeBytes * reference * 12n / 10n;
683
+ const actual = BigInt(tx.getFee?.() ?? 0);
684
+ if (actual > ceiling) {
685
+ throw new ValidationError(
686
+ `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.`
687
+ );
688
+ }
689
+ }
655
690
  function bn(value) {
656
691
  return new crypto.BN(value.toString());
657
692
  }
@@ -726,6 +761,7 @@ function buildTx(params) {
726
761
  }
727
762
  tx.sign(privKeys[0]);
728
763
  if (typeof tx.seal === "function") tx.seal();
764
+ assertSaneFee(tx, network);
729
765
  const hex = tx.toString();
730
766
  const resolvedOutputs = tx.outputs.map((o) => ({
731
767
  script: o.script.toHex(),
@@ -809,16 +845,14 @@ function parseTokenRef(scriptHex) {
809
845
  }
810
846
  return null;
811
847
  }
812
- function rebindOwner(scriptHex, toAddress, network) {
813
- const newPkh = Address.fromString(toAddress).hashBuffer.toString("hex");
814
- const re = /76a914[0-9a-f]{40}88ac/i;
815
- const matches = scriptHex.match(new RegExp(re, "gi"));
816
- if (!matches || matches.length !== 1) {
817
- throw new ValidationError(
818
- "rebindOwner: expected exactly one owner P2PKH in the token script"
819
- );
820
- }
821
- return scriptHex.replace(re, `76a914${newPkh}88ac`);
848
+ function tokenTransferScript(scriptHex, toAddress, network) {
849
+ const nft = parseNftScript(scriptHex);
850
+ if (nft.ref) return nftScript(toAddress, nft.ref, network);
851
+ const ft = parseFtScript(scriptHex);
852
+ if (ft.ref) return ftScript(toAddress, ft.ref, network);
853
+ throw new ValidationError(
854
+ "transferToken: tokenUtxo.script is not a recognised NFT or FT output script"
855
+ );
822
856
  }
823
857
  async function commitReveal(base, payload, refType, buildRevealOutputScript, tokenOutputValue) {
824
858
  const network = base.network ?? "mainnet";
@@ -932,8 +966,8 @@ async function transferToken(params) {
932
966
  "transferToken: tokenUtxo.script is required to rebuild the token output"
933
967
  );
934
968
  }
935
- const newScript = rebindOwner(tokenUtxo.script, params.toAddress);
936
- const ref = parseTokenRef(tokenUtxo.script);
969
+ const newScript = tokenTransferScript(tokenUtxo.script, params.toAddress, network);
970
+ const ref = parseNftScript(tokenUtxo.script).ref ?? parseFtScript(tokenUtxo.script).ref ?? null;
937
971
  const selection = selectRxdFunding(params.fundingUtxos, 0n, feeRate, {
938
972
  baseOutputCount: 1,
939
973
  // the token output
@@ -967,6 +1001,76 @@ async function transferToken(params) {
967
1001
  await params.client.broadcastTx(built.hex);
968
1002
  return { txid: built.txid, hex: built.hex, ref };
969
1003
  }
1004
+ async function transferFungible(params) {
1005
+ const network = params.network ?? "mainnet";
1006
+ const feeRate = params.feeRate ?? MIN_RELAY_FEE_RATE[network];
1007
+ const { amount, tokenUtxos } = params;
1008
+ if (amount <= 0n) {
1009
+ throw new ValidationError("transferFungible: amount must be positive");
1010
+ }
1011
+ if (!tokenUtxos.length) {
1012
+ throw new ValidationError("transferFungible: no token UTXOs given");
1013
+ }
1014
+ let ref;
1015
+ for (const u of tokenUtxos) {
1016
+ const parsed = u.script ? parseFtScript(u.script) : {};
1017
+ if (!parsed.ref) {
1018
+ throw new ValidationError(
1019
+ `transferFungible: ${u.txid}:${u.vout} is not an FT output (its script must be the on-chain ftScript)`
1020
+ );
1021
+ }
1022
+ if (ref && parsed.ref !== ref) {
1023
+ throw new ValidationError(
1024
+ "transferFungible: token UTXOs are for different tokens \u2014 refusing to mix refs, it would burn one of them"
1025
+ );
1026
+ }
1027
+ ref = parsed.ref;
1028
+ }
1029
+ const sorted = [...tokenUtxos].sort((a, b) => a.value < b.value ? 1 : a.value > b.value ? -1 : 0);
1030
+ const spend = [];
1031
+ let sum = 0n;
1032
+ for (const u of sorted) {
1033
+ spend.push(u);
1034
+ sum += u.value;
1035
+ if (sum >= amount) break;
1036
+ }
1037
+ if (sum < amount) {
1038
+ throw new ValidationError(
1039
+ `transferFungible: insufficient token balance \u2014 have ${sum}, need ${amount}`
1040
+ );
1041
+ }
1042
+ const change = sum - amount;
1043
+ const outputs = [{ script: ftScript(params.toAddress, ref, network), value: amount }];
1044
+ if (change > 0n) {
1045
+ outputs.push({ script: ftScript(params.address, ref, network), value: change });
1046
+ }
1047
+ const selection = selectRxdFunding(params.fundingUtxos, 0n, feeRate, {
1048
+ baseOutputCount: outputs.length,
1049
+ extraInputBytes: BigInt(148 * spend.length),
1050
+ // the token inputs we add below
1051
+ withChange: true
1052
+ });
1053
+ const inputs = [
1054
+ ...spend.map((u) => ({ txid: u.txid, vout: u.vout, value: u.value, script: u.script })),
1055
+ ...selection.inputs.map((u) => ({
1056
+ txid: u.txid,
1057
+ vout: u.vout,
1058
+ value: u.value,
1059
+ script: u.script || void 0
1060
+ }))
1061
+ ];
1062
+ const built = buildTx({
1063
+ address: params.address,
1064
+ wif: params.wif,
1065
+ inputs,
1066
+ outputs,
1067
+ addChange: true,
1068
+ feeRate,
1069
+ network
1070
+ });
1071
+ await params.client.broadcastTx(built.hex);
1072
+ return { txid: built.txid, hex: built.hex, ref, sent: amount, change };
1073
+ }
970
1074
 
971
1075
  // src/wave.ts
972
1076
  function waveLabel(name) {
@@ -1016,12 +1120,68 @@ async function waveResolveAddress(name, options = {}) {
1016
1120
  return r.registered ? r.address ?? null : null;
1017
1121
  }
1018
1122
 
1123
+ // src/discovery.ts
1124
+ var GLYPH_TOKEN_TYPE = {
1125
+ UNKNOWN: 0,
1126
+ FT: 1,
1127
+ NFT: 2,
1128
+ DAT: 3,
1129
+ DMINT: 4,
1130
+ WAVE: 5,
1131
+ CONTAINER: 6,
1132
+ AUTHORITY: 7
1133
+ };
1134
+ async function fetchTokenPage(path, params, options, fnName) {
1135
+ const base = (options.restBase ?? DEFAULT_REST_BASE).replace(/\/+$/, "");
1136
+ const doFetch = options.fetchImpl ?? globalThis.fetch;
1137
+ if (typeof doFetch !== "function") {
1138
+ throw new ValidationError(
1139
+ `${fnName}: no fetch implementation available; pass options.fetchImpl`
1140
+ );
1141
+ }
1142
+ const query = new URLSearchParams();
1143
+ for (const [k, v] of Object.entries(params)) {
1144
+ if (v !== void 0) query.set(k, String(v));
1145
+ }
1146
+ const qs = query.toString();
1147
+ const url = `${base}${path}${qs ? `?${qs}` : ""}`;
1148
+ const res = await doFetch(url, { signal: options.signal });
1149
+ if (!res.ok) {
1150
+ throw new ValidationError(`${fnName}: HTTP ${res.status} ${res.statusText}`);
1151
+ }
1152
+ const data = await res.json();
1153
+ return {
1154
+ tokens: Array.isArray(data?.tokens) ? data.tokens : [],
1155
+ nextCursor: data?.next_cursor ?? null
1156
+ };
1157
+ }
1158
+ async function getRecentTokens(options = {}) {
1159
+ return fetchTokenPage(
1160
+ "/glyphs/recent",
1161
+ { limit: options.limit, cursor: options.cursor, type_id: options.typeId },
1162
+ options,
1163
+ "getRecentTokens"
1164
+ );
1165
+ }
1166
+ async function getTokensByType(typeId, options = {}) {
1167
+ if (!Number.isInteger(typeId) || typeId < 0 || typeId > 7) {
1168
+ throw new ValidationError(`getTokensByType: invalid typeId ${typeId}`);
1169
+ }
1170
+ return fetchTokenPage(
1171
+ `/glyphs/by-type/${typeId}`,
1172
+ { limit: options.limit, cursor: options.cursor, order: options.order },
1173
+ options,
1174
+ "getTokensByType"
1175
+ );
1176
+ }
1177
+
1019
1178
  exports.DEFAULT_ELECTRUM_ENDPOINT = DEFAULT_ELECTRUM_ENDPOINT;
1020
1179
  exports.DEFAULT_REST_BASE = DEFAULT_REST_BASE;
1021
1180
  exports.DUST_LIMIT = DUST_LIMIT;
1022
1181
  exports.ElectrumClient = ElectrumClient;
1023
1182
  exports.ElectrumError = ElectrumError;
1024
1183
  exports.GLYPH_PROTOCOL = GLYPH_PROTOCOL;
1184
+ exports.GLYPH_TOKEN_TYPE = GLYPH_TOKEN_TYPE;
1025
1185
  exports.HDWallet = HDWallet;
1026
1186
  exports.InsufficientFundsError = InsufficientFundsError;
1027
1187
  exports.Keys = Keys;
@@ -1040,6 +1200,8 @@ exports.encodeGlyph = encodeGlyph;
1040
1200
  exports.estimateFee = estimateFee;
1041
1201
  exports.filterFundingCandidates = filterFundingCandidates;
1042
1202
  exports.ftScript = ftScript;
1203
+ exports.getRecentTokens = getRecentTokens;
1204
+ exports.getTokensByType = getTokensByType;
1043
1205
  exports.isFundingSafe = isFundingSafe;
1044
1206
  exports.isTokenBearing = isTokenBearing;
1045
1207
  exports.mintFT = mintFT;
@@ -1047,6 +1209,9 @@ exports.mintNFT = mintNFT;
1047
1209
  exports.nftScript = nftScript;
1048
1210
  exports.p2pkhScript = p2pkhScript;
1049
1211
  exports.packRef = packRef;
1212
+ exports.parseFtScript = parseFtScript;
1213
+ exports.parseNftScript = parseNftScript;
1214
+ exports.parseP2pkhScript = parseP2pkhScript;
1050
1215
  exports.parseTokenRef = parseTokenRef;
1051
1216
  exports.photonsToRxd = photonsToRxd;
1052
1217
  exports.radiantjs = radiantjs;
@@ -1054,6 +1219,8 @@ exports.rxdToPhotons = rxdToPhotons;
1054
1219
  exports.scriptHash = scriptHash;
1055
1220
  exports.selectRxdFunding = selectRxdFunding;
1056
1221
  exports.sumValue = sumValue;
1222
+ exports.tokenScriptKind = tokenScriptKind;
1223
+ exports.transferFungible = transferFungible;
1057
1224
  exports.transferToken = transferToken;
1058
1225
  exports.unpackRef = unpackRef;
1059
1226
  exports.waveLabel = waveLabel;