@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 +12 -0
- package/dist/index.cjs +179 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -4
- package/dist/index.d.ts +176 -4
- package/dist/index.js +172 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
const
|
|
816
|
-
if (
|
|
817
|
-
|
|
818
|
-
|
|
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 =
|
|
936
|
-
const ref =
|
|
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;
|