@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.
@@ -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
- const preferredIndex = this.providers.findIndex((p) => { var _a; return ((_a = p.name) !== null && _a !== void 0 ? _a : '').toLowerCase().includes('blockfrost'); });
73
- return this.getResponse('getAssetUtxo', [policyId, hex], preferredIndex >= 0 ? preferredIndex : undefined);
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
@@ -8,3 +8,4 @@ export * from './config';
8
8
  export * from './datum/imageDatum';
9
9
  export * from './providers/Koios';
10
10
  export * from './providers/Blockfrost';
11
+ export * from './datum/cip68Metadata';
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
- return await this.fetchBlockfrost(`/addresses/${bech32Address}/utxos`);
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 {};
@@ -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: (_c = (_b = output.inline_datum) === null || _b === void 0 ? void 0 : _b.bytes) !== null && _c !== void 0 ? _c : null,
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: (_e = (_d = output.reference_script) === null || _d === void 0 ? void 0 : _d.hash) !== null && _e !== void 0 ? _e : null,
125
- reference_script_cbor: (_g = (_f = output.reference_script) === null || _f === void 0 ? void 0 : _f.bytes) !== null && _g !== void 0 ? _g : null
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 (const output of result) {
211
- utxos.push(await this.buildChainProviderAsset(output));
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
- return utxos;
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 {};