@koralabs/kora-labs-common 6.16.0 → 6.18.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/chain/datum/cip68Metadata.d.ts +15 -0
- package/chain/datum/cip68Metadata.js +217 -0
- package/chain/failover/ChainProviderFailover.d.ts +6 -1
- package/chain/failover/ChainProviderFailover.js +24 -6
- package/chain/failover/interfaces.d.ts +33 -0
- package/chain/index.d.ts +1 -0
- package/chain/index.js +1 -0
- package/chain/providers/Blockfrost.d.ts +4 -1
- package/chain/providers/Blockfrost.js +54 -1
- package/chain/providers/Koios.d.ts +7 -2
- package/chain/providers/Koios.js +80 -11
- package/chain/transport/fetchProviderJson.d.ts +2 -0
- package/chain/transport/fetchProviderJson.js +4 -1
- package/package.json +1 -1
- package/testing/blockfrost.d.ts +12 -0
- package/testing/blockfrost.js +49 -0
- package/testing/index.d.ts +1 -0
- package/testing/index.js +1 -0
- package/testing/liveWallet.d.ts +16 -2
- package/testing/liveWallet.js +79 -61
- package/testing/onChain.d.ts +2 -6
- package/testing/onChain.js +26 -26
- package/txBuild/blockfrost.d.ts +6 -0
- package/txBuild/blockfrost.js +33 -26
- package/txBuild/scriptTx.d.ts +26 -0
- package/txBuild/scriptTx.js +43 -4
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type Cip68Standard = 'ft' | 'nft' | 'rft';
|
|
2
|
+
/** CIP-67 labels (with checksum) of the CIP-68 user tokens Blockfrost resolves, and of the reference NFT. */
|
|
3
|
+
export declare const CIP68_REFERENCE_LABEL = "000643b0";
|
|
4
|
+
/** For a CIP-68 user token (222/333/444) asset-name hex: its standard and reference-NFT asset-name hex. */
|
|
5
|
+
export declare const cip68ReferenceOf: (hex: string) => {
|
|
6
|
+
standard: Cip68Standard;
|
|
7
|
+
referenceHex: string;
|
|
8
|
+
} | null;
|
|
9
|
+
/** Port of the UTF-8 validator Blockfrost uses (websockets/utf-8-validate fallback). */
|
|
10
|
+
export declare const isValidUTF8: (buf: Buffer) => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Blockfrost's `onchain_metadata` for a CIP-68 token whose reference NFT carries `datumHex`, or null
|
|
13
|
+
* when the datum is not a CIP-68 `Constr [metadata_map, version, extra?]`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const cip68OnchainMetadata: (datumHex: string, standard: Cip68Standard) => Record<string, unknown> | null;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.cip68OnchainMetadata = exports.isValidUTF8 = exports.cip68ReferenceOf = exports.CIP68_REFERENCE_LABEL = void 0;
|
|
7
|
+
// CIP-68 reference-datum -> `onchain_metadata`, exactly as Blockfrost reports it for `/assets/{unit}`.
|
|
8
|
+
//
|
|
9
|
+
// Ported from blockfrost-utils `src/cip68.ts` (getMetadataFromOutputDatum / convertDatumValue,
|
|
10
|
+
// github.com/blockfrost/blockfrost-utils @ b467b2a). Providers without that endpoint (Koios) read the
|
|
11
|
+
// reference token's datum and convert it here, so every consumer sees ONE metadata shape whichever
|
|
12
|
+
// provider answered. Blockfrost's quirks are kept on purpose: only the CIP-68 schema fields of the
|
|
13
|
+
// token's standard are decoded (bytes -> UTF-8, or hex when not valid UTF-8); every other field keeps
|
|
14
|
+
// the value's ORIGINAL CBOR hex (consumers such as the HAL BFF decode those themselves).
|
|
15
|
+
const cbor_1 = __importDefault(require("cbor"));
|
|
16
|
+
const cbor_2 = require("../../tx/cbor");
|
|
17
|
+
const FILES = {
|
|
18
|
+
type: 'array',
|
|
19
|
+
optional: true,
|
|
20
|
+
items: {
|
|
21
|
+
name: { type: 'bytestring', optional: true },
|
|
22
|
+
mediaType: { type: 'bytestring' },
|
|
23
|
+
src: { type: 'bytestring' }
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const METADATA_SCHEME_MAP = {
|
|
27
|
+
ft: {
|
|
28
|
+
name: { type: 'bytestring' },
|
|
29
|
+
description: { type: 'bytestring' },
|
|
30
|
+
ticker: { type: 'bytestring', optional: true },
|
|
31
|
+
url: { type: 'bytestring', optional: true },
|
|
32
|
+
logo: { type: 'bytestring', optional: true },
|
|
33
|
+
decimals: { type: 'number', optional: true }
|
|
34
|
+
},
|
|
35
|
+
nft: {
|
|
36
|
+
name: { type: 'bytestring' },
|
|
37
|
+
image: { type: 'bytestring' },
|
|
38
|
+
mediaType: { type: 'bytestring', optional: true },
|
|
39
|
+
description: { type: 'bytestring', optional: true },
|
|
40
|
+
files: FILES
|
|
41
|
+
},
|
|
42
|
+
rft: {
|
|
43
|
+
name: { type: 'bytestring' },
|
|
44
|
+
image: { type: 'bytestring' },
|
|
45
|
+
mediaType: { type: 'bytestring', optional: true },
|
|
46
|
+
description: { type: 'bytestring', optional: true },
|
|
47
|
+
decimals: { type: 'number', optional: true },
|
|
48
|
+
files: FILES
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/** CIP-67 labels (with checksum) of the CIP-68 user tokens Blockfrost resolves, and of the reference NFT. */
|
|
52
|
+
exports.CIP68_REFERENCE_LABEL = '000643b0';
|
|
53
|
+
const USER_TOKEN_STANDARDS = { '000de140': 'nft', '0014df10': 'ft', '001bc280': 'rft' };
|
|
54
|
+
/** For a CIP-68 user token (222/333/444) asset-name hex: its standard and reference-NFT asset-name hex. */
|
|
55
|
+
const cip68ReferenceOf = (hex) => {
|
|
56
|
+
const standard = USER_TOKEN_STANDARDS[hex.slice(0, 8)];
|
|
57
|
+
return standard ? { standard, referenceHex: `${exports.CIP68_REFERENCE_LABEL}${hex.slice(8)}` } : null;
|
|
58
|
+
};
|
|
59
|
+
exports.cip68ReferenceOf = cip68ReferenceOf;
|
|
60
|
+
/** Port of the UTF-8 validator Blockfrost uses (websockets/utf-8-validate fallback). */
|
|
61
|
+
const isValidUTF8 = (buf) => {
|
|
62
|
+
const len = buf.length;
|
|
63
|
+
let i = 0;
|
|
64
|
+
while (i < len) {
|
|
65
|
+
if ((buf[i] & 0x80) === 0x00) {
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
else if ((buf[i] & 0xe0) === 0xc0) {
|
|
69
|
+
if (i + 1 === len || (buf[i + 1] & 0xc0) !== 0x80 || (buf[i] & 0xfe) === 0xc0)
|
|
70
|
+
return false;
|
|
71
|
+
i += 2;
|
|
72
|
+
}
|
|
73
|
+
else if ((buf[i] & 0xf0) === 0xe0) {
|
|
74
|
+
if (i + 2 >= len ||
|
|
75
|
+
(buf[i + 1] & 0xc0) !== 0x80 ||
|
|
76
|
+
(buf[i + 2] & 0xc0) !== 0x80 ||
|
|
77
|
+
(buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) ||
|
|
78
|
+
(buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
i += 3;
|
|
82
|
+
}
|
|
83
|
+
else if ((buf[i] & 0xf8) === 0xf0) {
|
|
84
|
+
if (i + 3 >= len ||
|
|
85
|
+
(buf[i + 1] & 0xc0) !== 0x80 ||
|
|
86
|
+
(buf[i + 2] & 0xc0) !== 0x80 ||
|
|
87
|
+
(buf[i + 3] & 0xc0) !== 0x80 ||
|
|
88
|
+
(buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) ||
|
|
89
|
+
(buf[i] === 0xf4 && buf[i + 1] > 0x8f) ||
|
|
90
|
+
buf[i] > 0xf4) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
i += 4;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
};
|
|
101
|
+
exports.isValidUTF8 = isValidUTF8;
|
|
102
|
+
const toUTF8OrHex = (buffer) => ((0, exports.isValidUTF8)(buffer) ? buffer.toString('utf8') : buffer.toString('hex'));
|
|
103
|
+
const convertDatumValue = (decodedValue, schema) => {
|
|
104
|
+
var _a;
|
|
105
|
+
if (!schema)
|
|
106
|
+
return null;
|
|
107
|
+
const scheme = schema;
|
|
108
|
+
if (scheme.type === 'number' && typeof decodedValue === 'number')
|
|
109
|
+
return decodedValue;
|
|
110
|
+
if (scheme.type === 'bytestring' && Buffer.isBuffer(decodedValue))
|
|
111
|
+
return toUTF8OrHex(decodedValue);
|
|
112
|
+
if (scheme.type === 'bytestring' && Array.isArray(decodedValue))
|
|
113
|
+
return toUTF8OrHex(Buffer.concat(decodedValue));
|
|
114
|
+
if (Array.isArray(decodedValue)) {
|
|
115
|
+
const converted = [];
|
|
116
|
+
for (const item of decodedValue) {
|
|
117
|
+
const value = convertDatumValue(item, (_a = scheme.items) !== null && _a !== void 0 ? _a : null);
|
|
118
|
+
if (value === null)
|
|
119
|
+
return null;
|
|
120
|
+
converted.push(value);
|
|
121
|
+
}
|
|
122
|
+
return converted;
|
|
123
|
+
}
|
|
124
|
+
if (decodedValue instanceof Map) {
|
|
125
|
+
const metadataMap = {};
|
|
126
|
+
for (const [key, mapValue] of decodedValue.entries()) {
|
|
127
|
+
const convertedKey = Buffer.isBuffer(key) ? toUTF8OrHex(key) : key;
|
|
128
|
+
const valueSchema = schema && convertedKey in schema ? schema[convertedKey] : null;
|
|
129
|
+
const value = convertDatumValue(mapValue, valueSchema);
|
|
130
|
+
if (value === null)
|
|
131
|
+
return null;
|
|
132
|
+
metadataMap[convertedKey] = value;
|
|
133
|
+
}
|
|
134
|
+
return metadataMap;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
/** Start/end of the constructor's fields list: `#6.121..127/1280..1400([fields])` or `#6.102([index, [fields]])`. */
|
|
139
|
+
const constrFields = (bytes) => {
|
|
140
|
+
const tag = (0, cbor_2.readCborHeader)(bytes, 0);
|
|
141
|
+
if (tag.majorType !== 6)
|
|
142
|
+
return null;
|
|
143
|
+
const isCompact = (tag.length >= 121 && tag.length <= 127) || (tag.length >= 1280 && tag.length <= 1400);
|
|
144
|
+
if (isCompact)
|
|
145
|
+
return tag.headerEnd;
|
|
146
|
+
if (tag.length !== 102)
|
|
147
|
+
return null;
|
|
148
|
+
const pair = (0, cbor_2.readCborHeader)(bytes, tag.headerEnd);
|
|
149
|
+
return pair.majorType === 4 ? (0, cbor_2.skipCborItem)(bytes, pair.headerEnd) : null;
|
|
150
|
+
};
|
|
151
|
+
/** The CBOR items of the list starting at `start` (definite or indefinite), as [start, end) spans. */
|
|
152
|
+
const listItems = (bytes, start) => {
|
|
153
|
+
const header = (0, cbor_2.readCborHeader)(bytes, start);
|
|
154
|
+
if (header.majorType !== 4)
|
|
155
|
+
return null;
|
|
156
|
+
const spans = [];
|
|
157
|
+
let cursor = header.headerEnd;
|
|
158
|
+
const indefinite = header.additionalInfo === 31;
|
|
159
|
+
for (let i = 0; indefinite ? bytes[cursor] !== 0xff : i < header.length; i++) {
|
|
160
|
+
const end = (0, cbor_2.skipCborItem)(bytes, cursor);
|
|
161
|
+
spans.push([cursor, end]);
|
|
162
|
+
cursor = end;
|
|
163
|
+
}
|
|
164
|
+
return spans;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Blockfrost's `onchain_metadata` for a CIP-68 token whose reference NFT carries `datumHex`, or null
|
|
168
|
+
* when the datum is not a CIP-68 `Constr [metadata_map, version, extra?]`.
|
|
169
|
+
*/
|
|
170
|
+
const cip68OnchainMetadata = (datumHex, standard) => {
|
|
171
|
+
const bytes = Buffer.from(datumHex, 'hex');
|
|
172
|
+
let fields;
|
|
173
|
+
try {
|
|
174
|
+
const fieldsStart = constrFields(bytes);
|
|
175
|
+
fields = fieldsStart === null ? null : listItems(bytes, fieldsStart);
|
|
176
|
+
}
|
|
177
|
+
catch (_a) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
// [metadata, version] are mandatory
|
|
181
|
+
if (!fields || fields.length < 2)
|
|
182
|
+
return null;
|
|
183
|
+
const [mapStart] = fields[0];
|
|
184
|
+
const map = (0, cbor_2.readCborHeader)(bytes, mapStart);
|
|
185
|
+
if (map.majorType !== 5)
|
|
186
|
+
return null;
|
|
187
|
+
const schema = METADATA_SCHEME_MAP[standard];
|
|
188
|
+
const metadata = {};
|
|
189
|
+
const seenKeys = new Set();
|
|
190
|
+
let cursor = map.headerEnd;
|
|
191
|
+
const indefinite = map.additionalInfo === 31;
|
|
192
|
+
for (let i = 0; indefinite ? bytes[cursor] !== 0xff : i < map.length; i++) {
|
|
193
|
+
const keyEnd = (0, cbor_2.skipCborItem)(bytes, cursor);
|
|
194
|
+
const valueEnd = (0, cbor_2.skipCborItem)(bytes, keyEnd);
|
|
195
|
+
const keyBytes = bytes.subarray(cursor, keyEnd);
|
|
196
|
+
const valueBytes = bytes.subarray(keyEnd, valueEnd);
|
|
197
|
+
cursor = valueEnd;
|
|
198
|
+
// CSL's PlutusMap groups a repeated key's values and Blockfrost reads the first one.
|
|
199
|
+
const keyId = keyBytes.toString('hex');
|
|
200
|
+
if (seenKeys.has(keyId))
|
|
201
|
+
continue;
|
|
202
|
+
seenKeys.add(keyId);
|
|
203
|
+
const decodedKey = cbor_1.default.decodeFirstSync(keyBytes);
|
|
204
|
+
const convertedKey = Buffer.isBuffer(decodedKey) ? toUTF8OrHex(decodedKey) : decodedKey;
|
|
205
|
+
const valueHex = valueBytes.toString('hex');
|
|
206
|
+
if (!(schema && convertedKey in schema)) {
|
|
207
|
+
// Custom field not covered by the CIP-68 standard: unparsed CBOR
|
|
208
|
+
metadata[convertedKey] = valueHex;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const converted = convertDatumValue(cbor_1.default.decodeFirstSync(valueBytes), schema[convertedKey]);
|
|
212
|
+
metadata[convertedKey] = converted !== null ? converted : valueHex;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return metadata;
|
|
216
|
+
};
|
|
217
|
+
exports.cip68OnchainMetadata = cip68OnchainMetadata;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccountAsset, AddressInfo, BackgroundImageDetails, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from './interfaces';
|
|
1
|
+
import { AccountAsset, AddressInfo, BackgroundImageDetails, ChainProtocolParameters, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from './interfaces';
|
|
2
2
|
export interface FailoverOptions {
|
|
3
3
|
/**
|
|
4
4
|
* Cooldown (ms) during which a provider that just failed is skipped as the *start* provider,
|
|
@@ -42,9 +42,14 @@ export declare class ChainProviderFailover {
|
|
|
42
42
|
getRefScriptCbor(tx: string, policyId: string, hex: string): Promise<string>;
|
|
43
43
|
getTxUtxos(tx: string): Promise<ChainProviderUtxo>;
|
|
44
44
|
getAssetUtxo(policyId: string, hex: string): Promise<ChainProviderAsset | null>;
|
|
45
|
+
/** Index of the Blockfrost provider, if configured (start provider for freshness-critical reads). */
|
|
46
|
+
private blockfrostIndex;
|
|
45
47
|
getAssetsByStakeKey(stakeKey: string): Promise<AccountAsset[]>;
|
|
46
48
|
getDatumFromHash(datumHash: string): Promise<string | null>;
|
|
47
49
|
getBackgroundImageDetails(policyId: string, hex: string): Promise<BackgroundImageDetails>;
|
|
48
50
|
getAddressInfo(bech32Address: string): Promise<AddressInfo>;
|
|
49
51
|
getAddressUTxOs(bech32Address: string): Promise<ChainProviderAsset[]>;
|
|
52
|
+
getProtocolParameters(): Promise<ChainProtocolParameters>;
|
|
53
|
+
getTxOutputConsumer(txHash: string, outputIndex: number): Promise<string | null>;
|
|
54
|
+
getAssetOnchainMetadata(policyId: string, hex: string): Promise<Record<string, unknown> | null>;
|
|
50
55
|
}
|
|
@@ -29,18 +29,22 @@ class ChainProviderFailover {
|
|
|
29
29
|
getNextProviderIndex(currentIndex) {
|
|
30
30
|
return (currentIndex + 1) % this.providers.length;
|
|
31
31
|
}
|
|
32
|
-
async getResponse(funcName, args, currentIndex, runNum) {
|
|
32
|
+
async getResponse(funcName, args, currentIndex, runNum, notFound) {
|
|
33
33
|
const num = runNum !== null && runNum !== void 0 ? runNum : 0;
|
|
34
34
|
const index = currentIndex !== null && currentIndex !== void 0 ? currentIndex : this.getStartIndex();
|
|
35
35
|
const chainProvider = this.providers[index];
|
|
36
36
|
return await chainProvider[funcName](...args).catch((error) => {
|
|
37
|
+
// A 404 is an answer ("not on chain (yet)"), not a provider failure: it still falls
|
|
38
|
+
// through, but without benching the provider, and it outranks another provider's outage.
|
|
39
|
+
const isNotFound = (error === null || error === void 0 ? void 0 : error.status) === 404;
|
|
37
40
|
// Mark the failed provider unhealthy for the cooldown window so it isn't chosen as the
|
|
38
41
|
// start provider again immediately; it remains a valid fallback.
|
|
39
|
-
if (this.cooldownMs > 0)
|
|
42
|
+
if (this.cooldownMs > 0 && !isNotFound)
|
|
40
43
|
this.unhealthyUntil[index] = this.now() + this.cooldownMs;
|
|
44
|
+
const answer = notFound !== null && notFound !== void 0 ? notFound : (isNotFound ? error : undefined);
|
|
41
45
|
if (num === this.providers.length - 1)
|
|
42
|
-
throw error;
|
|
43
|
-
return this.getResponse(funcName, args, this.getNextProviderIndex(index), num + 1);
|
|
46
|
+
throw answer !== null && answer !== void 0 ? answer : error;
|
|
47
|
+
return this.getResponse(funcName, args, this.getNextProviderIndex(index), num + 1, answer);
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
async getLatestTransactionForAsset(policyId, hex) {
|
|
@@ -69,8 +73,12 @@ class ChainProviderFailover {
|
|
|
69
73
|
// provider lagging a burn returns a stale UTxO and falsely resurrects a de-indexed handle.
|
|
70
74
|
// Prefer Blockfrost (reads the latest asset tx live; Koios's tx index can lag a burn by
|
|
71
75
|
// minutes). Koios stays the failover on error.
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
return this.getResponse('getAssetUtxo', [policyId, hex], this.blockfrostIndex());
|
|
77
|
+
}
|
|
78
|
+
/** Index of the Blockfrost provider, if configured (start provider for freshness-critical reads). */
|
|
79
|
+
blockfrostIndex() {
|
|
80
|
+
const index = this.providers.findIndex((p) => { var _a; return ((_a = p.name) !== null && _a !== void 0 ? _a : '').toLowerCase().includes('blockfrost'); });
|
|
81
|
+
return index >= 0 ? index : undefined;
|
|
74
82
|
}
|
|
75
83
|
async getAssetsByStakeKey(stakeKey) {
|
|
76
84
|
return this.getResponse('getAssetsByStakeKey', [stakeKey]);
|
|
@@ -87,5 +95,15 @@ class ChainProviderFailover {
|
|
|
87
95
|
async getAddressUTxOs(bech32Address) {
|
|
88
96
|
return this.getResponse('getAddressUTxOs', [bech32Address]);
|
|
89
97
|
}
|
|
98
|
+
async getProtocolParameters() {
|
|
99
|
+
return this.getResponse('getProtocolParameters', []);
|
|
100
|
+
}
|
|
101
|
+
async getTxOutputConsumer(txHash, outputIndex) {
|
|
102
|
+
// Blockfrost answers from its spent-by index in one call; Koios has to scan the address's txs.
|
|
103
|
+
return this.getResponse('getTxOutputConsumer', [txHash, outputIndex], this.blockfrostIndex());
|
|
104
|
+
}
|
|
105
|
+
async getAssetOnchainMetadata(policyId, hex) {
|
|
106
|
+
return this.getResponse('getAssetOnchainMetadata', [policyId, hex]);
|
|
107
|
+
}
|
|
90
108
|
}
|
|
91
109
|
exports.ChainProviderFailover = ChainProviderFailover;
|
|
@@ -28,6 +28,26 @@ export interface AddressInfo {
|
|
|
28
28
|
stake_address?: string | null;
|
|
29
29
|
script: boolean;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Current protocol parameters in Blockfrost's `/epochs/latest/parameters` field names (Koios's
|
|
33
|
+
* `/epoch_params` uses the same names and reports identical values; its `cost_models` is mapped to
|
|
34
|
+
* `cost_models_raw`). Cost models are ledger-ordered arrays keyed `PlutusV1`/`PlutusV2`/`PlutusV3`.
|
|
35
|
+
* `@koralabs/kora-labs-common/txBuild` `protocolParametersFromChain` turns this into tx-builder params.
|
|
36
|
+
*/
|
|
37
|
+
export interface ChainProtocolParameters {
|
|
38
|
+
epoch: number;
|
|
39
|
+
min_fee_a: number;
|
|
40
|
+
min_fee_b: number;
|
|
41
|
+
max_tx_size: number;
|
|
42
|
+
coins_per_utxo_size: string;
|
|
43
|
+
key_deposit: string;
|
|
44
|
+
price_mem: number;
|
|
45
|
+
price_step: number;
|
|
46
|
+
max_tx_ex_mem: string;
|
|
47
|
+
max_tx_ex_steps: string;
|
|
48
|
+
min_fee_ref_script_cost_per_byte: number | null;
|
|
49
|
+
cost_models_raw: Record<string, number[]>;
|
|
50
|
+
}
|
|
31
51
|
export interface BackgroundImageDetails {
|
|
32
52
|
image: string;
|
|
33
53
|
creatorDefaults?: ICreatorDefaults;
|
|
@@ -52,11 +72,24 @@ export interface ChainProvider {
|
|
|
52
72
|
getAssetDatum(policyId: string, hex: string): Promise<string | null>;
|
|
53
73
|
getCip25AssetImage(policyId: string, hex: string): Promise<string | null>;
|
|
54
74
|
getRefScriptCbor(tx: string, policyId: string, hex: string): Promise<string>;
|
|
75
|
+
/** The tx's outputs. Rejects with `status: 404` when the tx is not on chain (not in a block yet). */
|
|
55
76
|
getTxUtxos(tx: string): Promise<ChainProviderUtxo>;
|
|
56
77
|
getAssetUtxo(policyId: string, hex: string): Promise<ChainProviderAsset | null>;
|
|
57
78
|
getAssetsByStakeKey(stakeKey: string): Promise<AccountAsset[]>;
|
|
58
79
|
getDatumFromHash(datumHash: string): Promise<string | null>;
|
|
59
80
|
getBackgroundImageDetails(policyId: string, hex: string): Promise<BackgroundImageDetails>;
|
|
60
81
|
getAddressInfo(bech32Address: string): Promise<AddressInfo>;
|
|
82
|
+
/** Every unspent output at the address (all pages). An address with no UTxOs has none. */
|
|
61
83
|
getAddressUTxOs(bech32Address: string): Promise<ChainProviderAsset[]>;
|
|
84
|
+
getProtocolParameters(): Promise<ChainProtocolParameters>;
|
|
85
|
+
/**
|
|
86
|
+
* The tx that spent output `txHash#outputIndex`, or null while it is unspent. Rejects with
|
|
87
|
+
* `status: 404` when that output does not exist (e.g. the tx is not in a block yet).
|
|
88
|
+
*/
|
|
89
|
+
getTxOutputConsumer(txHash: string, outputIndex: number): Promise<string | null>;
|
|
90
|
+
/**
|
|
91
|
+
* The asset's `onchain_metadata` as Blockfrost reports it (CIP-68 reference datum converted per
|
|
92
|
+
* `cip68OnchainMetadata`, else the CIP-25 721 entry), or null when it has none.
|
|
93
|
+
*/
|
|
94
|
+
getAssetOnchainMetadata(policyId: string, hex: string): Promise<Record<string, unknown> | null>;
|
|
62
95
|
}
|
package/chain/index.d.ts
CHANGED
package/chain/index.js
CHANGED
|
@@ -27,3 +27,4 @@ __exportStar(require("./config"), exports);
|
|
|
27
27
|
__exportStar(require("./datum/imageDatum"), exports);
|
|
28
28
|
__exportStar(require("./providers/Koios"), exports);
|
|
29
29
|
__exportStar(require("./providers/Blockfrost"), exports);
|
|
30
|
+
__exportStar(require("./datum/cip68Metadata"), exports);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ICreatorDefaults } from '../../handles/interfaces';
|
|
2
|
-
import { AccountAsset, AddressInfo, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from '../failover/interfaces';
|
|
2
|
+
import { AccountAsset, AddressInfo, ChainProtocolParameters, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from '../failover/interfaces';
|
|
3
3
|
import { ChainProviderConfig } from '../providerConfig';
|
|
4
4
|
export interface DrepMetadataValue {
|
|
5
5
|
'@value': string;
|
|
@@ -55,5 +55,8 @@ export declare class Blockfrost implements ChainProvider {
|
|
|
55
55
|
}>;
|
|
56
56
|
getAddressInfo(bech32Address: string): Promise<AddressInfo>;
|
|
57
57
|
getAddressUTxOs(bech32Address: string): Promise<ChainProviderAsset[]>;
|
|
58
|
+
getProtocolParameters(): Promise<ChainProtocolParameters>;
|
|
59
|
+
getTxOutputConsumer(txHash: string, outputIndex: number): Promise<string | null>;
|
|
60
|
+
getAssetOnchainMetadata(policyId: string, hex: string): Promise<Record<string, unknown> | null>;
|
|
58
61
|
}
|
|
59
62
|
export {};
|
|
@@ -138,7 +138,60 @@ class Blockfrost {
|
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
async getAddressUTxOs(bech32Address) {
|
|
141
|
-
|
|
141
|
+
// Blockfrost pages at 100: never truncate. An address that never received funds is a 404.
|
|
142
|
+
const all = [];
|
|
143
|
+
for (let page = 1;; page++) {
|
|
144
|
+
let batch;
|
|
145
|
+
try {
|
|
146
|
+
batch = await this.fetchBlockfrost(`/addresses/${bech32Address}/utxos?count=100&page=${page}`);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if ((error === null || error === void 0 ? void 0 : error.status) === 404)
|
|
150
|
+
return all;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
all.push(...batch);
|
|
154
|
+
if (batch.length < 100)
|
|
155
|
+
return all;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async getProtocolParameters() {
|
|
159
|
+
var _a;
|
|
160
|
+
const p = await this.fetchBlockfrost('epochs/latest/parameters');
|
|
161
|
+
return {
|
|
162
|
+
epoch: p.epoch,
|
|
163
|
+
min_fee_a: p.min_fee_a,
|
|
164
|
+
min_fee_b: p.min_fee_b,
|
|
165
|
+
max_tx_size: p.max_tx_size,
|
|
166
|
+
coins_per_utxo_size: String(p.coins_per_utxo_size),
|
|
167
|
+
key_deposit: String(p.key_deposit),
|
|
168
|
+
price_mem: p.price_mem,
|
|
169
|
+
price_step: p.price_step,
|
|
170
|
+
max_tx_ex_mem: String(p.max_tx_ex_mem),
|
|
171
|
+
max_tx_ex_steps: String(p.max_tx_ex_steps),
|
|
172
|
+
min_fee_ref_script_cost_per_byte: (_a = p.min_fee_ref_script_cost_per_byte) !== null && _a !== void 0 ? _a : null,
|
|
173
|
+
cost_models_raw: p.cost_models_raw
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async getTxOutputConsumer(txHash, outputIndex) {
|
|
177
|
+
var _a;
|
|
178
|
+
const { outputs } = await this.getTxUtxos(txHash);
|
|
179
|
+
const output = outputs.find((o) => o.output_index === outputIndex);
|
|
180
|
+
if (!output)
|
|
181
|
+
throw (0, fetchProviderJson_1.providerNotFoundError)('Blockfrost', `Output ${txHash}#${outputIndex}`);
|
|
182
|
+
return (_a = output.consumed_by_tx) !== null && _a !== void 0 ? _a : null;
|
|
183
|
+
}
|
|
184
|
+
async getAssetOnchainMetadata(policyId, hex) {
|
|
185
|
+
var _a;
|
|
186
|
+
try {
|
|
187
|
+
const asset = await this.fetchBlockfrost(`/assets/${policyId}${hex}`);
|
|
188
|
+
return (_a = asset === null || asset === void 0 ? void 0 : asset.onchain_metadata) !== null && _a !== void 0 ? _a : null;
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if ((error === null || error === void 0 ? void 0 : error.status) === 404)
|
|
192
|
+
return null;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
142
195
|
}
|
|
143
196
|
}
|
|
144
197
|
exports.Blockfrost = Blockfrost;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ICreatorDefaults } from '../../handles/interfaces';
|
|
2
|
-
import { AccountAsset, AddressInfo, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from '../failover/interfaces';
|
|
2
|
+
import { AccountAsset, AddressInfo, ChainProtocolParameters, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from '../failover/interfaces';
|
|
3
3
|
import { ChainProviderConfig } from '../providerConfig';
|
|
4
4
|
interface KoiosAsset {
|
|
5
5
|
quantity: string;
|
|
@@ -17,9 +17,11 @@ interface KoiosTxOutput {
|
|
|
17
17
|
bytes: string | null;
|
|
18
18
|
value: string | null;
|
|
19
19
|
} | null;
|
|
20
|
-
payment_addr
|
|
20
|
+
/** tx_info outputs carry `payment_addr`; address_utxos / utxo_info rows carry `address` instead. */
|
|
21
|
+
payment_addr?: {
|
|
21
22
|
bech32: string;
|
|
22
23
|
};
|
|
24
|
+
address?: string;
|
|
23
25
|
reference_script: {
|
|
24
26
|
bytes: string | null;
|
|
25
27
|
hash: string | null;
|
|
@@ -64,5 +66,8 @@ export declare class Koios implements ChainProvider {
|
|
|
64
66
|
}>;
|
|
65
67
|
getAddressInfo(bech32Address: string): Promise<AddressInfo>;
|
|
66
68
|
getAddressUTxOs(bech32Address: string): Promise<ChainProviderAsset[]>;
|
|
69
|
+
getProtocolParameters(): Promise<ChainProtocolParameters>;
|
|
70
|
+
getTxOutputConsumer(txHash: string, outputIndex: number): Promise<string | null>;
|
|
71
|
+
getAssetOnchainMetadata(policyId: string, hex: string): Promise<Record<string, unknown> | null>;
|
|
67
72
|
}
|
|
68
73
|
export {};
|
package/chain/providers/Koios.js
CHANGED
|
@@ -4,7 +4,12 @@ exports.Koios = exports.buildKoiosAddressUtxoRequestBody = exports.buildKoiosHea
|
|
|
4
4
|
const types_1 = require("../../types");
|
|
5
5
|
const fetchProviderJson_1 = require("../transport/fetchProviderJson");
|
|
6
6
|
const imageDatum_1 = require("../datum/imageDatum");
|
|
7
|
+
const cip68Metadata_1 = require("../datum/cip68Metadata");
|
|
7
8
|
const providerConfig_1 = require("../providerConfig");
|
|
9
|
+
/** PostgREST page size Koios serves (its maximum). */
|
|
10
|
+
const KOIOS_PAGE = 1000;
|
|
11
|
+
/** Tx hashes per tx_info request when scanning for an output's consumer. */
|
|
12
|
+
const TX_INFO_BATCH = 50;
|
|
8
13
|
const buildKoiosHeaders = (bearerToken) => ({
|
|
9
14
|
'Content-Type': 'application/json',
|
|
10
15
|
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {})
|
|
@@ -58,7 +63,7 @@ class Koios {
|
|
|
58
63
|
}, [])) !== null && _a !== void 0 ? _a : []);
|
|
59
64
|
}
|
|
60
65
|
async getAssetUtxo(policyId, hex) {
|
|
61
|
-
var _a, _b, _c;
|
|
66
|
+
var _a, _b, _c, _d;
|
|
62
67
|
const tx = await this.getLatestTransactionForAsset(policyId, hex);
|
|
63
68
|
if (!tx)
|
|
64
69
|
return null;
|
|
@@ -74,9 +79,9 @@ class Koios {
|
|
|
74
79
|
}));
|
|
75
80
|
amount.push({ unit: 'lovelace', quantity: output.value });
|
|
76
81
|
const chainProviderAsset = {
|
|
77
|
-
address: output.payment_addr.bech32,
|
|
82
|
+
address: (_b = output.address) !== null && _b !== void 0 ? _b : output.payment_addr.bech32,
|
|
78
83
|
amount,
|
|
79
|
-
inline_datum: (
|
|
84
|
+
inline_datum: (_d = (_c = output.inline_datum) === null || _c === void 0 ? void 0 : _c.bytes) !== null && _d !== void 0 ? _d : null,
|
|
80
85
|
output_index: output.tx_index,
|
|
81
86
|
tx_hash: firstTransaction.tx_hash,
|
|
82
87
|
data_hash: output.datum_hash
|
|
@@ -105,7 +110,7 @@ class Koios {
|
|
|
105
110
|
return (_a = output === null || output === void 0 ? void 0 : output.reference_script_cbor) !== null && _a !== void 0 ? _a : '';
|
|
106
111
|
}
|
|
107
112
|
async buildChainProviderAsset(output) {
|
|
108
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
113
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
109
114
|
const amount = output.asset_list.map((a) => ({
|
|
110
115
|
unit: `${a.policy_id}${a.asset_name}`,
|
|
111
116
|
quantity: a.quantity
|
|
@@ -115,20 +120,22 @@ class Koios {
|
|
|
115
120
|
? await this.getDatumFromHash(output.datum_hash)
|
|
116
121
|
: (_c = (_b = output.inline_datum) === null || _b === void 0 ? void 0 : _b.bytes) !== null && _c !== void 0 ? _c : null;
|
|
117
122
|
return {
|
|
118
|
-
address: output.payment_addr.bech32,
|
|
123
|
+
address: (_d = output.address) !== null && _d !== void 0 ? _d : output.payment_addr.bech32,
|
|
119
124
|
amount,
|
|
120
125
|
inline_datum,
|
|
121
126
|
output_index: output.tx_index,
|
|
122
127
|
tx_hash: output.tx_hash,
|
|
123
128
|
data_hash: output.datum_hash,
|
|
124
|
-
reference_script_hash: (
|
|
125
|
-
reference_script_cbor: (
|
|
129
|
+
reference_script_hash: (_f = (_e = output.reference_script) === null || _e === void 0 ? void 0 : _e.hash) !== null && _f !== void 0 ? _f : null,
|
|
130
|
+
reference_script_cbor: (_h = (_g = output.reference_script) === null || _g === void 0 ? void 0 : _g.bytes) !== null && _h !== void 0 ? _h : null
|
|
126
131
|
};
|
|
127
132
|
}
|
|
128
133
|
async getTxUtxos(tx) {
|
|
129
134
|
var _a;
|
|
130
135
|
const res = await this.fetchKoios(`/tx_info`, 'POST', JSON.stringify({ _tx_hashes: [tx], _metadata: true, _assets: true }));
|
|
131
136
|
const [firstTransaction] = res;
|
|
137
|
+
if (!firstTransaction)
|
|
138
|
+
throw (0, fetchProviderJson_1.providerNotFoundError)('Koios', `Transaction ${tx}`);
|
|
132
139
|
const outputs = [];
|
|
133
140
|
for (const output of (_a = firstTransaction === null || firstTransaction === void 0 ? void 0 : firstTransaction.outputs) !== null && _a !== void 0 ? _a : []) {
|
|
134
141
|
outputs.push(await this.buildChainProviderAsset(output));
|
|
@@ -205,12 +212,74 @@ class Koios {
|
|
|
205
212
|
};
|
|
206
213
|
}
|
|
207
214
|
async getAddressUTxOs(bech32Address) {
|
|
208
|
-
const result = await this.fetchKoios(`/address_utxos`, 'POST', JSON.stringify((0, exports.buildKoiosAddressUtxoRequestBody)(bech32Address)));
|
|
209
215
|
const utxos = [];
|
|
210
|
-
for (
|
|
211
|
-
|
|
216
|
+
for (let offset = 0;; offset += KOIOS_PAGE) {
|
|
217
|
+
const page = await this.fetchKoios(`/address_utxos?offset=${offset}&limit=${KOIOS_PAGE}`, 'POST', JSON.stringify((0, exports.buildKoiosAddressUtxoRequestBody)(bech32Address)));
|
|
218
|
+
for (const output of page) {
|
|
219
|
+
utxos.push(await this.buildChainProviderAsset(output));
|
|
220
|
+
}
|
|
221
|
+
if (page.length < KOIOS_PAGE)
|
|
222
|
+
return utxos;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async getProtocolParameters() {
|
|
226
|
+
var _a;
|
|
227
|
+
// The tip's epoch, not the newest epoch_params row: rows for the next epoch can appear before it starts.
|
|
228
|
+
const [tip] = await this.fetchKoios('/tip');
|
|
229
|
+
const [p] = await this.fetchKoios(`/epoch_params?_epoch_no=${tip.epoch_no}`);
|
|
230
|
+
if (!p)
|
|
231
|
+
throw (0, fetchProviderJson_1.providerNotFoundError)('Koios', `Protocol parameters for epoch ${tip.epoch_no}`);
|
|
232
|
+
return {
|
|
233
|
+
epoch: p.epoch_no,
|
|
234
|
+
min_fee_a: p.min_fee_a,
|
|
235
|
+
min_fee_b: p.min_fee_b,
|
|
236
|
+
max_tx_size: p.max_tx_size,
|
|
237
|
+
coins_per_utxo_size: String(p.coins_per_utxo_size),
|
|
238
|
+
key_deposit: String(p.key_deposit),
|
|
239
|
+
price_mem: p.price_mem,
|
|
240
|
+
price_step: p.price_step,
|
|
241
|
+
max_tx_ex_mem: String(p.max_tx_ex_mem),
|
|
242
|
+
max_tx_ex_steps: String(p.max_tx_ex_steps),
|
|
243
|
+
min_fee_ref_script_cost_per_byte: (_a = p.min_fee_ref_script_cost_per_byte) !== null && _a !== void 0 ? _a : null,
|
|
244
|
+
cost_models_raw: p.cost_models
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async getTxOutputConsumer(txHash, outputIndex) {
|
|
248
|
+
const ref = `${txHash}#${outputIndex}`;
|
|
249
|
+
const [utxo] = await this.fetchKoios('/utxo_info', 'POST', JSON.stringify({ _utxo_refs: [ref], _extended: false }));
|
|
250
|
+
if (!utxo)
|
|
251
|
+
throw (0, fetchProviderJson_1.providerNotFoundError)('Koios', `Output ${ref}`);
|
|
252
|
+
if (!utxo.is_spent)
|
|
253
|
+
return null;
|
|
254
|
+
// Koios has no spent-by index: scan the txs touching the output's address from its block on
|
|
255
|
+
// (inclusive: a chained spend can land in the same block) for the one that consumes it.
|
|
256
|
+
for (let offset = 0;; offset += KOIOS_PAGE) {
|
|
257
|
+
const txs = await this.fetchKoios(`/address_txs?order=block_height.asc&offset=${offset}&limit=${KOIOS_PAGE}`, 'POST', JSON.stringify({ _addresses: [utxo.address], _after_block_height: utxo.block_height }));
|
|
258
|
+
const candidates = txs.map((t) => t.tx_hash).filter((h) => h !== txHash);
|
|
259
|
+
for (let i = 0; i < candidates.length; i += TX_INFO_BATCH) {
|
|
260
|
+
const infos = await this.fetchKoios('/tx_info', 'POST', JSON.stringify({ _tx_hashes: candidates.slice(i, i + TX_INFO_BATCH), _inputs: true, _metadata: false, _assets: false, _withdrawals: false, _certs: false, _scripts: false, _bytecode: false, _governance: false }));
|
|
261
|
+
const consumer = infos.find((t) => { var _a; return (_a = t.inputs) === null || _a === void 0 ? void 0 : _a.some((input) => input.tx_hash === txHash && input.tx_index === outputIndex); });
|
|
262
|
+
if (consumer)
|
|
263
|
+
return consumer.tx_hash;
|
|
264
|
+
}
|
|
265
|
+
if (txs.length < KOIOS_PAGE)
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
throw new Error(`Koios reports ${ref} spent but no tx at ${utxo.address} consumes it`);
|
|
269
|
+
}
|
|
270
|
+
async getAssetOnchainMetadata(policyId, hex) {
|
|
271
|
+
var _a, _b, _c, _d;
|
|
272
|
+
// Blockfrost's precedence: a CIP-68 user token's reference datum, else the CIP-25 721 entry.
|
|
273
|
+
const cip68 = (0, cip68Metadata_1.cip68ReferenceOf)(hex);
|
|
274
|
+
if (cip68) {
|
|
275
|
+
const datum = await this.getAssetDatum(policyId, cip68.referenceHex);
|
|
276
|
+
const metadata = datum ? (0, cip68Metadata_1.cip68OnchainMetadata)(datum, cip68.standard) : null;
|
|
277
|
+
if (metadata)
|
|
278
|
+
return metadata;
|
|
212
279
|
}
|
|
213
|
-
|
|
280
|
+
const [info] = await this.fetchKoios('/asset_info', 'POST', JSON.stringify({ _asset_list: [[policyId, hex]] }));
|
|
281
|
+
const byPolicy = (_b = (_a = info === null || info === void 0 ? void 0 : info.minting_tx_metadata) === null || _a === void 0 ? void 0 : _a['721']) === null || _b === void 0 ? void 0 : _b[policyId];
|
|
282
|
+
return (_d = (_c = byPolicy === null || byPolicy === void 0 ? void 0 : byPolicy[hex]) !== null && _c !== void 0 ? _c : byPolicy === null || byPolicy === void 0 ? void 0 : byPolicy[Buffer.from(hex, 'hex').toString('utf8')]) !== null && _d !== void 0 ? _d : null;
|
|
214
283
|
}
|
|
215
284
|
}
|
|
216
285
|
exports.Koios = Koios;
|
|
@@ -25,6 +25,8 @@ export interface ProviderRequestOptions {
|
|
|
25
25
|
maxRateLimitWaitMs?: number;
|
|
26
26
|
fetcher?: ProviderFetcher;
|
|
27
27
|
}
|
|
28
|
+
/** A provider's "that does not exist" answer, shaped like a transport 404 (`status: 404`). */
|
|
29
|
+
export declare const providerNotFoundError: (provider: ProviderName, what: string) => Error;
|
|
28
30
|
export declare const isRetriableError: (provider: ProviderName, error: any) => boolean;
|
|
29
31
|
export declare const fetchProviderJson: <T>({ provider, url, method, headers, body, maxRetries, retryBaseDelayMs, maxRps, rateLimitKey, maxRateLimitWaitMs, fetcher }: ProviderRequestOptions) => Promise<T>;
|
|
30
32
|
export {};
|