@koralabs/kora-labs-common 6.17.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 {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fetchProviderJson = exports.isRetriableError = void 0;
3
+ exports.fetchProviderJson = exports.isRetriableError = exports.providerNotFoundError = void 0;
4
4
  const utils_1 = require("../../utils");
5
5
  const rateLimit_1 = require("./rateLimit");
6
6
  const DEFAULT_MAX_RPS = 5;
@@ -104,6 +104,9 @@ const createProviderError = ({ provider, status, statusText, payload, responseTe
104
104
  }
105
105
  return error;
106
106
  };
107
+ /** A provider's "that does not exist" answer, shaped like a transport 404 (`status: 404`). */
108
+ const providerNotFoundError = (provider, what) => createProviderError({ provider, status: 404, statusText: `${what} not found` });
109
+ exports.providerNotFoundError = providerNotFoundError;
107
110
  const messageLooksRetriable = (value) => RETRIABLE_MESSAGE_SNIPPETS.some((snippet) => value.includes(snippet));
108
111
  const isRetriableError = (provider, error) => {
109
112
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koralabs/kora-labs-common",
3
- "version": "6.17.0",
3
+ "version": "6.18.0",
4
4
  "description": "Kora Labs Common Utilities",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,5 +1,6 @@
1
1
  import { Cardano } from '@cardano-sdk/core';
2
2
  import { ProviderRequestOptions } from '../chain/transport/fetchProviderJson';
3
+ import { ChainProtocolParameters } from '../chain/failover/interfaces';
3
4
  import { ExUnits } from './fees';
4
5
  import { RedeemerKey, ScriptTxProtocolParameters } from './scriptTx';
5
6
  type BlockfrostAmount = {
@@ -30,6 +31,11 @@ export declare const toOgmiosUtxo: ([txIn, txOut]: Cardano.Utxo) => ({
30
31
  txId?: undefined;
31
32
  index?: undefined;
32
33
  })[];
34
+ /**
35
+ * Tx-builder parameters from provider protocol parameters (Blockfrost `/epochs/latest/parameters`, or
36
+ * any `ChainProvider.getProtocolParameters()`, e.g. through `ChainProviderFailover`).
37
+ */
38
+ export declare const protocolParametersFromChain: (p: ChainProtocolParameters) => ScriptTxProtocolParameters;
33
39
  export declare class BlockfrostTxClient {
34
40
  private readonly config;
35
41
  private readonly scripts;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BlockfrostTxClient = exports.toOgmiosUtxo = exports.valueFromBlockfrost = exports.BlockfrostEvaluationError = void 0;
3
+ exports.BlockfrostTxClient = exports.protocolParametersFromChain = exports.toOgmiosUtxo = exports.valueFromBlockfrost = exports.BlockfrostEvaluationError = void 0;
4
4
  // Blockfrost access for building/evaluating/submitting script transactions, returning @cardano-sdk/core
5
5
  // types. Every call goes through the shared rate-limited transport (fetchProviderJson), which honors
6
6
  // Retry-After and friends process-wide.
@@ -59,6 +59,37 @@ const toOgmiosUtxo = ([txIn, txOut]) => {
59
59
  ];
60
60
  };
61
61
  exports.toOgmiosUtxo = toOgmiosUtxo;
62
+ /**
63
+ * Tx-builder parameters from provider protocol parameters (Blockfrost `/epochs/latest/parameters`, or
64
+ * any `ChainProvider.getProtocolParameters()`, e.g. through `ChainProviderFailover`).
65
+ */
66
+ const protocolParametersFromChain = (p) => {
67
+ var _a;
68
+ const raw = p.cost_models_raw;
69
+ if (!raw)
70
+ throw new Error('Protocol parameters carry no cost_models_raw');
71
+ // cost_models_raw is the LEDGER-ordered array; the named `cost_models` object is not (its
72
+ // alphabetical order produced script_data_hash mismatches once the models were extended).
73
+ const costModels = new Map();
74
+ for (const [name, model] of Object.entries(raw)) {
75
+ const language = PLUTUS_LANGUAGE[name.charAt(0).toLowerCase() + name.slice(1)];
76
+ if (language !== undefined)
77
+ costModels.set(language, model);
78
+ }
79
+ return {
80
+ minFeeA: BigInt(p.min_fee_a),
81
+ minFeeB: BigInt(p.min_fee_b),
82
+ priceMemory: (0, fees_1.ratio)(p.price_mem),
83
+ priceSteps: (0, fees_1.ratio)(p.price_step),
84
+ minFeeRefScriptCostPerByte: (0, fees_1.ratio)((_a = p.min_fee_ref_script_cost_per_byte) !== null && _a !== void 0 ? _a : 0),
85
+ coinsPerUtxoByte: BigInt(p.coins_per_utxo_size),
86
+ stakeKeyDeposit: BigInt(p.key_deposit),
87
+ maxTxSize: Number(p.max_tx_size),
88
+ maxTxExUnits: { memory: Number(p.max_tx_ex_mem), steps: Number(p.max_tx_ex_steps) },
89
+ costModels
90
+ };
91
+ };
92
+ exports.protocolParametersFromChain = protocolParametersFromChain;
62
93
  class BlockfrostTxClient {
63
94
  constructor(config) {
64
95
  this.config = config;
@@ -89,31 +120,7 @@ class BlockfrostTxClient {
89
120
  }
90
121
  }
91
122
  async getProtocolParameters() {
92
- var _a;
93
- const p = await this.request('epochs/latest/parameters');
94
- const raw = p.cost_models_raw;
95
- if (!raw)
96
- throw new Error('Protocol parameters carry no cost_models_raw');
97
- // cost_models_raw is the LEDGER-ordered array; the named `cost_models` object is not (its
98
- // alphabetical order produced script_data_hash mismatches once the models were extended).
99
- const costModels = new Map();
100
- for (const [name, model] of Object.entries(raw)) {
101
- const language = PLUTUS_LANGUAGE[name.charAt(0).toLowerCase() + name.slice(1)];
102
- if (language !== undefined)
103
- costModels.set(language, model);
104
- }
105
- return {
106
- minFeeA: BigInt(p.min_fee_a),
107
- minFeeB: BigInt(p.min_fee_b),
108
- priceMemory: (0, fees_1.ratio)(p.price_mem),
109
- priceSteps: (0, fees_1.ratio)(p.price_step),
110
- minFeeRefScriptCostPerByte: (0, fees_1.ratio)((_a = p.min_fee_ref_script_cost_per_byte) !== null && _a !== void 0 ? _a : 0),
111
- coinsPerUtxoByte: BigInt(p.coins_per_utxo_size),
112
- stakeKeyDeposit: BigInt(p.key_deposit),
113
- maxTxSize: Number(p.max_tx_size),
114
- maxTxExUnits: { memory: Number(p.max_tx_ex_mem), steps: Number(p.max_tx_ex_steps) },
115
- costModels
116
- };
123
+ return (0, exports.protocolParametersFromChain)(await this.request('epochs/latest/parameters'));
117
124
  }
118
125
  getLatestBlock() {
119
126
  return this.request('blocks/latest');
@@ -73,6 +73,11 @@ export declare const computeScriptDataHash: ({ redeemersCbor, datumsCbor, costMo
73
73
  }) => string | undefined;
74
74
  /** Lovelace a certificate locks (positive) or releases (negative). */
75
75
  export declare const certificateDeposit: (certificate: Cardano.Certificate, stakeKeyDeposit: bigint) => bigint;
76
+ /** The inputs cannot pay the outputs + fee (+ a min-UTxO change output); `shortfall` more lovelace would. */
77
+ export declare class InsufficientInputsError extends Error {
78
+ readonly shortfall: bigint;
79
+ constructor(message: string, shortfall: bigint);
80
+ }
76
81
  export declare const finalizeScriptTx: (plan: ScriptTxPlan, params: ScriptTxProtocolParameters, evaluate: Evaluator) => Promise<FinalizedScriptTx>;
77
82
  /**
78
83
  * Wallet inputs for a tx whose other inputs are fixed: ADA-only UTxOs first, largest first, until they
@@ -80,6 +85,27 @@ export declare const finalizeScriptTx: (plan: ScriptTxPlan, params: ScriptTxProt
80
85
  * UTxOs carrying tokens are only used when ADA-only ones do not suffice (their tokens go to change).
81
86
  */
82
87
  export declare const selectWalletInputs: (utxos: Cardano.Utxo[], lovelace: bigint, exclude?: Set<string>) => Cardano.Utxo[];
88
+ export interface WalletFundedPlan extends Omit<ScriptTxPlan, 'inputs' | 'signerCount'> {
89
+ /** Inputs the tx spends regardless of funding (scripts, tokens being moved); none for a plain payment. */
90
+ inputs?: ScriptTxPlan['inputs'];
91
+ /**
92
+ * The wallet's UTxOs exactly as the wallet handed them over (CIP-30 `getUtxos`), which may include
93
+ * its own UNCONFIRMED change (tx chaining). They are never looked up on chain.
94
+ */
95
+ walletUtxos: Cardano.Utxo[];
96
+ /** vkey witnesses the signed tx carries once these wallet inputs are chosen. */
97
+ signerCount: (walletInputs: Cardano.Utxo[]) => number;
98
+ }
99
+ /**
100
+ * Coin selection + finalization in one: walks the `selectWalletInputs` order (ADA-only first, largest
101
+ * first) one UTxO at a time and returns the first selection `finalizeScriptTx` can balance: inputs
102
+ * covering outputs + fee with a min-UTxO change output. Pure: no chain reads, so chained (unconfirmed)
103
+ * wallet UTxOs are spendable. Throws `InsufficientInputsError` (shortfall vs. the whole wallet) when
104
+ * no selection works.
105
+ */
106
+ export declare const finalizeWalletFundedTx: ({ walletUtxos, signerCount, inputs, ...plan }: WalletFundedPlan, params: ScriptTxProtocolParameters, evaluate: Evaluator) => Promise<FinalizedScriptTx & {
107
+ walletInputs: Cardano.Utxo[];
108
+ }>;
83
109
  /**
84
110
  * Add vkey witnesses to an unsigned tx by splicing them into its witness set byte-for-byte
85
111
  * (`mergeWitnessSet`): the body (so the tx id and every signature) and the redeemer bytes the
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.addVkeyWitnesses = exports.selectWalletInputs = exports.finalizeScriptTx = exports.certificateDeposit = exports.computeScriptDataHash = exports.withMinAda = void 0;
3
+ exports.addVkeyWitnesses = exports.finalizeWalletFundedTx = exports.selectWalletInputs = exports.finalizeScriptTx = exports.InsufficientInputsError = exports.certificateDeposit = exports.computeScriptDataHash = exports.withMinAda = void 0;
4
4
  // Deterministic Plutus transaction finalizer on @cardano-sdk/core (no input selection, no Helios).
5
5
  //
6
6
  // The caller fixes every input; this module derives redeemer pointers from the ledger's canonical
@@ -87,6 +87,15 @@ const sumValues = (values) => {
87
87
  }
88
88
  return { coins, assets };
89
89
  };
90
+ /** The inputs cannot pay the outputs + fee (+ a min-UTxO change output); `shortfall` more lovelace would. */
91
+ class InsufficientInputsError extends Error {
92
+ constructor(message, shortfall) {
93
+ super(message);
94
+ this.shortfall = shortfall;
95
+ this.name = 'InsufficientInputsError';
96
+ }
97
+ }
98
+ exports.InsufficientInputsError = InsufficientInputsError;
90
99
  const finalizeScriptTx = async (plan, params, evaluate) => {
91
100
  var _a, _b, _c;
92
101
  const inputs = [...plan.inputs].sort((a, b) => byTxIn(a.utxo[0], b.utxo[0]));
@@ -112,7 +121,7 @@ const finalizeScriptTx = async (plan, params, evaluate) => {
112
121
  var _a, _b, _c;
113
122
  const coins = inValue.coins + withdrawn - deposited - produced.coins - fee;
114
123
  if (coins < BigInt(0))
115
- throw new Error(`Inputs do not cover outputs + fee: ${-coins} lovelace short`);
124
+ throw new InsufficientInputsError(`Inputs do not cover outputs + fee: ${-coins} lovelace short`, -coins);
116
125
  const assets = new Map();
117
126
  const ids = new Set([...inValue.assets.keys(), ...tokenMap.keys(), ...produced.assets.keys()]);
118
127
  for (const id of ids) {
@@ -192,8 +201,9 @@ const finalizeScriptTx = async (plan, params, evaluate) => {
192
201
  throw new Error('Execution units did not converge');
193
202
  }
194
203
  const { tx, change, redeemers } = assemble(fee, exUnits, new Map());
195
- if (change.value.coins < (0, fees_1.minAdaForOutputSize)(params.coinsPerUtxoByte, outputSize(change))) {
196
- throw new Error(`Inputs do not cover outputs + fee: change of ${change.value.coins} lovelace is below min-UTxO`);
204
+ const changeMinAda = (0, fees_1.minAdaForOutputSize)(params.coinsPerUtxoByte, outputSize(change));
205
+ if (change.value.coins < changeMinAda) {
206
+ throw new InsufficientInputsError(`Inputs do not cover outputs + fee: change of ${change.value.coins} lovelace is below min-UTxO`, changeMinAda - change.value.coins);
197
207
  }
198
208
  const signedSize = sizeOf(fee, exUnits);
199
209
  if (signedSize > params.maxTxSize)
@@ -231,6 +241,35 @@ const selectWalletInputs = (utxos, lovelace, exclude = new Set()) => {
231
241
  return selected;
232
242
  };
233
243
  exports.selectWalletInputs = selectWalletInputs;
244
+ const coinsOf = (utxos) => utxos.reduce((sum, [, out]) => sum + out.value.coins, BigInt(0));
245
+ /**
246
+ * Coin selection + finalization in one: walks the `selectWalletInputs` order (ADA-only first, largest
247
+ * first) one UTxO at a time and returns the first selection `finalizeScriptTx` can balance: inputs
248
+ * covering outputs + fee with a min-UTxO change output. Pure: no chain reads, so chained (unconfirmed)
249
+ * wallet UTxOs are spendable. Throws `InsufficientInputsError` (shortfall vs. the whole wallet) when
250
+ * no selection works.
251
+ */
252
+ const finalizeWalletFundedTx = async ({ walletUtxos, signerCount, inputs = [], ...plan }, params, evaluate) => {
253
+ const available = coinsOf(walletUtxos);
254
+ let required = plan.outputs.reduce((sum, o) => sum + o.value.coins, BigInt(0));
255
+ // Each selection is the previous one plus the next UTxO in order (+1 lovelace past its total).
256
+ for (let lovelace = BigInt(1); lovelace <= available;) {
257
+ const walletInputs = (0, exports.selectWalletInputs)(walletUtxos, lovelace);
258
+ const selected = coinsOf(walletInputs);
259
+ try {
260
+ const tx = await (0, exports.finalizeScriptTx)({ ...plan, inputs: [...inputs, ...walletInputs.map((utxo) => ({ utxo }))], signerCount: signerCount(walletInputs) }, params, evaluate);
261
+ return { ...tx, walletInputs };
262
+ }
263
+ catch (error) {
264
+ if (!(error instanceof InsufficientInputsError))
265
+ throw error;
266
+ required = selected + error.shortfall;
267
+ lovelace = selected + BigInt(1);
268
+ }
269
+ }
270
+ throw new InsufficientInputsError(`Wallet holds ${available} spendable lovelace; about ${required} needed`, required - available);
271
+ };
272
+ exports.finalizeWalletFundedTx = finalizeWalletFundedTx;
234
273
  /**
235
274
  * Add vkey witnesses to an unsigned tx by splicing them into its witness set byte-for-byte
236
275
  * (`mergeWitnessSet`): the body (so the tx id and every signature) and the redeemer bytes the