@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.
@@ -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.16.0",
3
+ "version": "6.18.0",
4
4
  "description": "Kora Labs Common Utilities",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,12 @@
1
+ export type LiveNetwork = 'preview' | 'preprod' | 'mainnet';
2
+ export interface BlockfrostAccess {
3
+ network: LiveNetwork;
4
+ blockfrostApiKey: string;
5
+ /** Injectable for tests. Defaults to global fetch. */
6
+ fetchFn?: typeof fetch;
7
+ }
8
+ export declare const blockfrostBaseUrl: (network: LiveNetwork) => string;
9
+ /** GET `path`; null when Blockfrost answers 404 (never used / not on chain yet). */
10
+ export declare const blockfrostGet: <T>(access: BlockfrostAccess, path: string) => Promise<T | null>;
11
+ /** POST a signed tx to `/tx/submit`; resolves to the tx hash, or throws with the node's reason. */
12
+ export declare const blockfrostSubmit: (access: BlockfrostAccess, signedTxCbor: string) => Promise<string>;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.blockfrostSubmit = exports.blockfrostGet = exports.blockfrostBaseUrl = void 0;
4
+ /**
5
+ * Blockfrost access for the live-suite harness. Every call goes through the shared rate-limited
6
+ * transport (`chain/transport/fetchProviderJson`): a stated wait (Retry-After and friends) is
7
+ * remembered process-wide and never undercut, and a wait too long to sit out surfaces as
8
+ * `RateLimitedError` instead of a retry.
9
+ */
10
+ const fetchProviderJson_1 = require("../chain/transport/fetchProviderJson");
11
+ const blockfrostBaseUrl = (network) => `https://cardano-${network}.blockfrost.io/api/v0`;
12
+ exports.blockfrostBaseUrl = blockfrostBaseUrl;
13
+ const request = (access, path, init = {}) => {
14
+ var _a;
15
+ return (0, fetchProviderJson_1.fetchProviderJson)({
16
+ provider: 'Blockfrost',
17
+ url: `${(0, exports.blockfrostBaseUrl)(access.network)}${path}`,
18
+ method: (_a = init.method) !== null && _a !== void 0 ? _a : 'GET',
19
+ headers: { project_id: access.blockfrostApiKey, ...(init.contentType ? { 'Content-Type': init.contentType } : {}) },
20
+ body: init.body,
21
+ fetcher: access.fetchFn
22
+ });
23
+ };
24
+ /** GET `path`; null when Blockfrost answers 404 (never used / not on chain yet). */
25
+ const blockfrostGet = async (access, path) => {
26
+ try {
27
+ return await request(access, path);
28
+ }
29
+ catch (error) {
30
+ if (error.status === 404)
31
+ return null;
32
+ throw error;
33
+ }
34
+ };
35
+ exports.blockfrostGet = blockfrostGet;
36
+ /** POST a signed tx to `/tx/submit`; resolves to the tx hash, or throws with the node's reason. */
37
+ const blockfrostSubmit = async (access, signedTxCbor) => {
38
+ var _a, _b, _c;
39
+ try {
40
+ return await request(access, '/tx/submit', { method: 'POST', body: Buffer.from(signedTxCbor, 'hex'), contentType: 'application/cbor' });
41
+ }
42
+ catch (error) {
43
+ const e = error;
44
+ if (e.status === undefined)
45
+ throw error;
46
+ throw new Error(`Submit failed: ${e.status} ${(_c = (_b = (_a = e.blockfrostResponse) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : e.responseText) !== null && _c !== void 0 ? _c : ''}`.trim(), { cause: error });
47
+ }
48
+ };
49
+ exports.blockfrostSubmit = blockfrostSubmit;
@@ -1,3 +1,4 @@
1
+ export * from './blockfrost';
1
2
  export * from './liveWallet';
2
3
  export * from './installWallet';
3
4
  export * from './readOnlyGuard';
package/testing/index.js CHANGED
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  // Subpath module: `@koralabs/kora-labs-common/testing`. Node-only harness for live CIP-30 browser
18
18
  // journey suites. NOT re-exported from the root index. Requires the optional peer dependencies
19
19
  // @cardano-sdk/core, @cardano-sdk/crypto, bip39 (and @emurgo/cardano-message-signing-nodejs for signData).
20
+ __exportStar(require("./blockfrost"), exports);
20
21
  __exportStar(require("./liveWallet"), exports);
21
22
  __exportStar(require("./installWallet"), exports);
22
23
  __exportStar(require("./readOnlyGuard"), exports);
@@ -1,4 +1,4 @@
1
- export type LiveNetwork = 'preview' | 'preprod' | 'mainnet';
1
+ import { LiveNetwork } from './blockfrost';
2
2
  export interface LiveWalletConfig {
3
3
  mnemonic: string;
4
4
  network: LiveNetwork;
@@ -10,6 +10,17 @@ export interface LiveWalletConfig {
10
10
  /** Injectable for tests. Defaults to global fetch. */
11
11
  fetchFn?: typeof fetch;
12
12
  }
13
+ /** One tx this wallet submitted, with the chaining facts a suite asserts on. */
14
+ export interface LiveSubmission {
15
+ txHash: string;
16
+ /** Local clock (ms) when the submission was accepted. */
17
+ submittedAt: number;
18
+ /**
19
+ * Inputs (`txHash#index`) that were outputs of this wallet's OWN earlier submissions that were not
20
+ * in a block yet when this tx was submitted — i.e. the tx is chained on unconfirmed change.
21
+ */
22
+ chainedInputs: string[];
23
+ }
13
24
  export interface LiveWallet {
14
25
  address: string;
15
26
  addressHex: string;
@@ -21,7 +32,10 @@ export interface LiveWallet {
21
32
  dRepId: string;
22
33
  call: (method: string, params?: unknown) => Promise<unknown>;
23
34
  submittedTxHashes: string[];
35
+ /** Every accepted submission, in order (`submittedTxHashes` is the hash-only view). */
36
+ submissions: LiveSubmission[];
37
+ /** Hashes of submitted txs this wallet has not yet seen in a block. */
38
+ pendingTxHashes: () => string[];
24
39
  countUtxos: () => Promise<number>;
25
40
  }
26
- export declare const blockfrostBaseUrl: (network: LiveNetwork) => string;
27
41
  export declare const createLiveWallet: (config: LiveWalletConfig) => Promise<LiveWallet>;
@@ -33,13 +33,17 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.createLiveWallet = exports.blockfrostBaseUrl = void 0;
36
+ exports.createLiveWallet = void 0;
37
37
  /**
38
38
  * Live CIP-30 wallet emulator for browser journey suites (Playwright).
39
39
  *
40
40
  * Real keys derived from a mnemonic (CIP-1852), real UTxOs from Blockfrost, real signatures, real
41
41
  * submission. It behaves like a real CIP-30 wallet on purpose:
42
42
  * - getUtxos returns the FULL paginated UTxO set (no hidden/"parked" collateral),
43
+ * - it CHAINS like Eternl/Lace: once it has submitted a tx, getUtxos (and getBalance, and the
44
+ * signing-key choice) return that tx's outputs to this wallet and drop the inputs it spent, until
45
+ * the tx is in a block — then chain truth takes over. A dapp therefore receives UTxOs that exist
46
+ * on no provider yet, exactly as real users' wallets hand them over (HAL minting depends on it),
43
47
  * - getCollateral returns null (the common real-wallet case — the app must resolve collateral),
44
48
  * - signTx returns a witness set holding ONLY this wallet's vkey witnesses (the app merges it),
45
49
  * - signTx refuses non-canonical tx structure (hardware wallets / Eternl do).
@@ -48,6 +52,7 @@ exports.createLiveWallet = exports.blockfrostBaseUrl = void 0;
48
52
  const blakejs_1 = require("blakejs");
49
53
  const bech32_1 = require("bech32");
50
54
  const tx_1 = require("../tx");
55
+ const blockfrost_1 = require("./blockfrost");
51
56
  const HARDENED = 0x80000000;
52
57
  let sdkPromise = null;
53
58
  // @cardano-sdk/crypto ships its own nested libsodium-wrappers-sumo; it must be `ready` before any
@@ -67,15 +72,12 @@ const loadSdk = () => {
67
72
  })());
68
73
  return sdkPromise;
69
74
  };
70
- const blockfrostBaseUrl = (network) => `https://cardano-${network}.blockfrost.io/api/v0`;
71
- exports.blockfrostBaseUrl = blockfrostBaseUrl;
72
75
  const createLiveWallet = async (config) => {
73
- var _a, _b;
76
+ var _a;
74
77
  const { Cardano, Serialization, Crypto, bip39 } = await loadSdk();
75
- const doFetch = (_a = config.fetchFn) !== null && _a !== void 0 ? _a : fetch;
78
+ const access = { network: config.network, blockfrostApiKey: config.blockfrostApiKey, fetchFn: config.fetchFn };
76
79
  const networkId = config.network === 'mainnet' ? 1 : 0;
77
- const base = (0, exports.blockfrostBaseUrl)(config.network);
78
- const account = (_b = config.accountIndex) !== null && _b !== void 0 ? _b : 0;
80
+ const account = (_a = config.accountIndex) !== null && _a !== void 0 ? _a : 0;
79
81
  const root = Crypto.Bip32PrivateKey.fromBip39Entropy(Buffer.from(bip39.mnemonicToEntropy(config.mnemonic), 'hex'), '');
80
82
  const accountKey = root.derive([1852 + HARDENED, 1815 + HARDENED, account + HARDENED]);
81
83
  const paymentKey = accountKey.derive([0, 0]).toRawKey();
@@ -96,25 +98,17 @@ const createLiveWallet = async (config) => {
96
98
  const dRepPubKeyHex = stakeKey.toPublic().hex();
97
99
  const dRepKeyHash = (0, blakejs_1.blake2b)(Buffer.from(dRepPubKeyHex, 'hex'), undefined, 28);
98
100
  const dRepId = bech32_1.bech32.encode('drep', bech32_1.bech32.toWords(Uint8Array.from([0x22, ...dRepKeyHash])), 128);
101
+ const ownAddresses = new Set([address, enterpriseStakeAddress]);
99
102
  const submittedTxHashes = [];
100
- const fetchBlockfrost = async (path) => {
101
- const res = await doFetch(`${base}${path}`, { headers: { project_id: config.blockfrostApiKey } });
102
- if (!res.ok)
103
- throw new Error(`Blockfrost ${path}: ${res.status} ${(await res.text()).slice(0, 200)}`);
104
- return res;
105
- };
103
+ const submissions = [];
104
+ const pending = new Map();
106
105
  const fetchAddressUtxos = async (addr) => {
107
106
  const all = [];
108
107
  for (let page = 1;; page++) {
109
- const res = await doFetch(`${base}/addresses/${addr}/utxos?count=100&page=${page}`, {
110
- headers: { project_id: config.blockfrostApiKey }
111
- });
112
- // 404 = the address has never been used — an empty set, not an error.
113
- if (res.status === 404)
108
+ // null (404) = the address has never been used — an empty set, not an error.
109
+ const batch = await (0, blockfrost_1.blockfrostGet)(access, `/addresses/${addr}/utxos?count=100&page=${page}`);
110
+ if (!batch)
114
111
  break;
115
- if (!res.ok)
116
- throw new Error(`Blockfrost /addresses/${addr}/utxos: ${res.status} ${(await res.text()).slice(0, 200)}`);
117
- const batch = (await res.json());
118
112
  for (const u of batch)
119
113
  u._sourceAddr = addr;
120
114
  all.push(...batch);
@@ -138,30 +132,50 @@ const createLiveWallet = async (config) => {
138
132
  };
139
133
  return new Serialization.TransactionUnspentOutput(Serialization.TransactionInput.fromCore({ txId: u.tx_hash, index: u.output_index }), Serialization.TransactionOutput.fromCore(output)).toCbor();
140
134
  };
141
- const getUtxos = async () => {
142
- const utxos = [...(await fetchAddressUtxos(address)), ...(await fetchAddressUtxos(enterpriseStakeAddress))];
143
- return utxos.map(utxoToCborHex);
135
+ /** A pending tx that is now in a block leaves the overlay: chain truth takes over. */
136
+ const settlePending = async () => {
137
+ for (const txHash of [...pending.keys()]) {
138
+ if (await (0, blockfrost_1.blockfrostGet)(access, `/txs/${txHash}`))
139
+ pending.delete(txHash);
140
+ }
141
+ };
142
+ /** The wallet's own view: chain UTxOs, minus what its pending txs spent, plus their unspent change. */
143
+ const ownUtxos = async () => {
144
+ var _a, _b, _c;
145
+ await settlePending();
146
+ const chainUtxos = [...(await fetchAddressUtxos(address)), ...(await fetchAddressUtxos(enterpriseStakeAddress))];
147
+ const spent = new Set([...pending.values()].flatMap((p) => [...p.spends]));
148
+ const view = new Map();
149
+ for (const u of chainUtxos) {
150
+ const ref = `${u.tx_hash}#${u.output_index}`;
151
+ if (spent.has(ref))
152
+ continue;
153
+ view.set(ref, { cbor: utxoToCborHex(u), address: (_a = u._sourceAddr) !== null && _a !== void 0 ? _a : address, lovelace: BigInt((_c = (_b = u.amount.find((a) => a.unit === 'lovelace')) === null || _b === void 0 ? void 0 : _b.quantity) !== null && _c !== void 0 ? _c : '0') });
154
+ }
155
+ for (const p of pending.values()) {
156
+ for (const [ref, out] of p.outputs)
157
+ if (!spent.has(ref))
158
+ view.set(ref, out);
159
+ }
160
+ return view;
144
161
  };
162
+ const getUtxos = async () => [...(await ownUtxos()).values()].map((u) => u.cbor);
145
163
  const getBalance = async () => {
146
- const utxos = await fetchAddressUtxos(address);
147
- const coins = utxos.reduce((sum, u) => { var _a, _b; return sum + BigInt((_b = (_a = u.amount.find((a) => a.unit === 'lovelace')) === null || _a === void 0 ? void 0 : _a.quantity) !== null && _b !== void 0 ? _b : '0'); }, BigInt(0));
164
+ const coins = [...(await ownUtxos()).values()].filter((u) => u.address === address).reduce((sum, u) => sum + u.lovelace, BigInt(0));
148
165
  return Serialization.Value.fromCore({ coins }).toCbor();
149
166
  };
150
- // The stake key must sign when it is a required signer, or when the tx spends/collateralizes a
151
- // UTxO whose PAYMENT credential is the stake key (EnterpriseAddress(stakeCredential)).
167
+ // The stake key must sign when it is a required signer, or when the tx spends/collateralizes one of
168
+ // this wallet's UTxOs whose PAYMENT credential is the stake key (EnterpriseAddress(stakeCredential)).
169
+ // Resolved from the wallet's own view, like a real wallet: chained inputs are on no provider yet.
152
170
  const needsStakeKeySignature = async (body) => {
153
- var _a, _b, _c, _d;
171
+ var _a, _b, _c;
154
172
  if (((_a = body.requiredExtraSignatures) !== null && _a !== void 0 ? _a : []).some((s) => s === stakeKeyHash))
155
173
  return true;
156
174
  const inputs = [...((_b = body.inputs) !== null && _b !== void 0 ? _b : []), ...((_c = body.collaterals) !== null && _c !== void 0 ? _c : [])];
157
- for (const input of inputs) {
158
- const res = await fetchBlockfrost(`/txs/${input.txId}/utxos`);
159
- const json = (await res.json());
160
- const out = (_d = json.outputs) === null || _d === void 0 ? void 0 : _d.find((o, i) => { var _a; return ((_a = o.output_index) !== null && _a !== void 0 ? _a : i) === input.index; });
161
- if ((out === null || out === void 0 ? void 0 : out.address) === enterpriseStakeAddress)
162
- return true;
163
- }
164
- return false;
175
+ if (inputs.length === 0)
176
+ return false;
177
+ const view = await ownUtxos();
178
+ return inputs.some((input) => { var _a; return ((_a = view.get(`${input.txId}#${input.index}`)) === null || _a === void 0 ? void 0 : _a.address) === enterpriseStakeAddress; });
165
179
  };
166
180
  const signTx = async (txCborHex) => {
167
181
  (0, tx_1.assertCanonicalCbor)(txCborHex, 'signTx');
@@ -181,6 +195,23 @@ const createLiveWallet = async (config) => {
181
195
  out.push(await signTx(typeof entry === 'string' ? entry : ((_a = entry.cbor) !== null && _a !== void 0 ? _a : entry.tx)));
182
196
  return out;
183
197
  };
198
+ /** Record an accepted submission: its spent inputs leave the view, its own outputs join it. */
199
+ const trackSubmitted = (txHash, signedTxCbor) => {
200
+ const body = Serialization.Transaction.fromCbor(signedTxCbor).body();
201
+ const spends = new Set(body.inputs().values().map((i) => `${i.transactionId()}#${Number(i.index())}`));
202
+ const chainedInputs = [...spends].filter((ref) => pending.has(ref.split('#')[0]));
203
+ const outputs = new Map();
204
+ body.outputs().forEach((output, index) => {
205
+ const outAddress = output.address().toBech32();
206
+ if (!ownAddresses.has(outAddress))
207
+ return;
208
+ const cbor = new Serialization.TransactionUnspentOutput(Serialization.TransactionInput.fromCore({ txId: txHash, index }), output).toCbor();
209
+ outputs.set(`${txHash}#${index}`, { cbor, address: outAddress, lovelace: output.amount().coin() });
210
+ });
211
+ pending.set(txHash, { spends, outputs });
212
+ submittedTxHashes.push(txHash);
213
+ submissions.push({ txHash, submittedAt: Date.now(), chainedInputs });
214
+ };
184
215
  const submitTx = async (signedTxCbor) => {
185
216
  if (config.submitDumpDir) {
186
217
  const fs = await Promise.resolve().then(() => __importStar(require('node:fs')));
@@ -188,29 +219,14 @@ const createLiveWallet = async (config) => {
188
219
  fs.mkdirSync(config.submitDumpDir, { recursive: true });
189
220
  fs.writeFileSync(path.join(config.submitDumpDir, `submit-${Date.now()}.cbor.hex`), signedTxCbor);
190
221
  }
191
- // Retry only transient failures (429/5xx). Any other 4xx is a real ledger rejection and
192
- // surfaces immediately with Blockfrost's full reason.
193
- const backoffsMs = [0, 1000, 4000, 10000];
194
- let lastError = '';
195
- for (const wait of backoffsMs) {
196
- if (wait)
197
- await new Promise((r) => setTimeout(r, wait));
198
- const res = await doFetch(`${base}/tx/submit`, {
199
- method: 'POST',
200
- headers: { project_id: config.blockfrostApiKey, 'Content-Type': 'application/cbor' },
201
- body: Buffer.from(signedTxCbor, 'hex')
202
- });
203
- const text = await res.text();
204
- if (res.ok) {
205
- const hash = text.replace(/"/g, '');
206
- submittedTxHashes.push(hash);
207
- return hash;
208
- }
209
- lastError = `${res.status} ${text.slice(0, 4000)}`;
210
- if (!(res.status === 429 || res.status >= 500))
211
- break;
212
- }
213
- throw new Error(`Submit failed: ${lastError}`);
222
+ // Transient failures (5xx) are retried by the transport; rate limits wait exactly the stated
223
+ // time; any other 4xx is a real ledger rejection and surfaces with Blockfrost's reason.
224
+ const hash = await (0, blockfrost_1.blockfrostSubmit)(access, signedTxCbor);
225
+ const expected = (0, tx_1.txHashFromCbor)(signedTxCbor);
226
+ if (hash !== expected)
227
+ throw new Error(`Submit returned ${hash} for tx ${expected}`);
228
+ trackSubmitted(hash, signedTxCbor);
229
+ return hash;
214
230
  };
215
231
  // CIP-8 COSE_Sign1 over the payload with the payment key (CIP-30 signData).
216
232
  const signData = async (addrHex, payloadHex) => {
@@ -279,7 +295,9 @@ const createLiveWallet = async (config) => {
279
295
  dRepId,
280
296
  call,
281
297
  submittedTxHashes,
282
- countUtxos: async () => (await fetchAddressUtxos(address)).length
298
+ submissions,
299
+ pendingTxHashes: () => [...pending.keys()],
300
+ countUtxos: async () => [...(await ownUtxos()).values()].filter((u) => u.address === address).length
283
301
  };
284
302
  };
285
303
  exports.createLiveWallet = createLiveWallet;
@@ -1,14 +1,10 @@
1
- import { LiveNetwork } from './liveWallet';
1
+ import { BlockfrostAccess } from './blockfrost';
2
2
  export declare const LBL_222 = "000de140";
3
3
  export declare const LBL_100 = "000643b0";
4
4
  export declare const LBL_000 = "00000000";
5
5
  export declare const LBL_444 = "001bc280";
6
6
  export declare const hexOf: (s: string) => string;
7
- export interface ChainConfig {
8
- network: LiveNetwork;
9
- blockfrostApiKey: string;
10
- fetchFn?: typeof fetch;
11
- }
7
+ export type ChainConfig = BlockfrostAccess;
12
8
  export interface TxUtxoOutput {
13
9
  address: string;
14
10
  amount: {
@@ -6,22 +6,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.cip68Field = exports.fetchCip68ExtraFromTx = exports.fetchOutputDatumCbor = exports.assertAdaTransfer = exports.assertAssetBurned = exports.assertCip68Label = exports.waitForOutputConsumed = exports.waitForTxConfirmation = exports.lovelaceOf = exports.fetchTxUtxos = exports.hexOf = exports.LBL_444 = exports.LBL_000 = exports.LBL_100 = exports.LBL_222 = void 0;
7
7
  /**
8
8
  * On-chain EFFECT assertions for live journey suites. Chain truth via Blockfrost — never the
9
- * indexer or the UI. Each assertion throws on mismatch so the calling scope fails.
9
+ * indexer or the UI. Each assertion throws on mismatch so the calling scope fails. Blockfrost is
10
+ * reached through the shared rate-limited transport (./blockfrost).
10
11
  */
11
12
  const cbor_1 = __importDefault(require("cbor"));
12
- const liveWallet_1 = require("./liveWallet");
13
+ const rateLimit_1 = require("../chain/transport/rateLimit");
14
+ const blockfrost_1 = require("./blockfrost");
13
15
  exports.LBL_222 = '000de140'; // CIP-68 user/owner NFT
14
16
  exports.LBL_100 = '000643b0'; // CIP-68 reference token
15
17
  exports.LBL_000 = '00000000'; // virtual subhandle token
16
18
  exports.LBL_444 = '001bc280'; // CIP-68 RFT
17
19
  const hexOf = (s) => Buffer.from(s, 'utf8').toString('hex');
18
20
  exports.hexOf = hexOf;
19
- const get = async (chain, path) => { var _a; return ((_a = chain.fetchFn) !== null && _a !== void 0 ? _a : fetch)(`${(0, liveWallet_1.blockfrostBaseUrl)(chain.network)}${path}`, { headers: { project_id: chain.blockfrostApiKey } }); };
20
21
  const getJson = async (chain, path) => {
21
- const res = await get(chain, path);
22
- if (!res.ok)
23
- throw new Error(`onChain: GET ${path} failed (${res.status})`);
24
- return (await res.json());
22
+ const found = await (0, blockfrost_1.blockfrostGet)(chain, path);
23
+ if (found === null)
24
+ throw new Error(`onChain: GET ${path} failed (404)`);
25
+ return found;
25
26
  };
26
27
  const fetchTxUtxos = (chain, txHash) => getJson(chain, `/txs/${txHash}/utxos`);
27
28
  exports.fetchTxUtxos = fetchTxUtxos;
@@ -31,11 +32,14 @@ exports.lovelaceOf = lovelaceOf;
31
32
  const waitForTxConfirmation = async (chain, txHash, timeoutMs = 180000, pollMs = 5000) => {
32
33
  const start = Date.now();
33
34
  for (;;) {
34
- const res = await get(chain, `/txs/${txHash}`).catch(() => null);
35
- if (res === null || res === void 0 ? void 0 : res.ok) {
36
- const data = (await res.json());
35
+ // A transient provider failure is another poll; a rate limit too long to sit out is returned.
36
+ const data = await (0, blockfrost_1.blockfrostGet)(chain, `/txs/${txHash}`).catch((error) => {
37
+ if (error instanceof rateLimit_1.RateLimitedError)
38
+ throw error;
39
+ return null;
40
+ });
41
+ if (data)
37
42
  return { confirmed: true, block: data.block, validContract: data.valid_contract };
38
- }
39
43
  if (Date.now() - start >= timeoutMs)
40
44
  return { confirmed: false };
41
45
  await new Promise((r) => setTimeout(r, pollMs));
@@ -62,31 +66,27 @@ exports.waitForOutputConsumed = waitForOutputConsumed;
62
66
  const assertCip68Label = async (chain, policyId, name, opts) => {
63
67
  var _a, _b;
64
68
  const nameHex = (0, exports.hexOf)(name);
65
- const present = await get(chain, `/assets/${policyId}${opts.hasLabel}${nameHex}`);
66
- if (!present.ok)
67
- throw new Error(`onChain: expected asset ${opts.hasLabel}+${name} under ${policyId} (${present.status})`);
68
- const quantity = BigInt((_a = (await present.json()).quantity) !== null && _a !== void 0 ? _a : '0');
69
+ const present = await (0, blockfrost_1.blockfrostGet)(chain, `/assets/${policyId}${opts.hasLabel}${nameHex}`);
70
+ if (!present)
71
+ throw new Error(`onChain: expected asset ${opts.hasLabel}+${name} under ${policyId} (not found)`);
72
+ const quantity = BigInt((_a = present.quantity) !== null && _a !== void 0 ? _a : '0');
69
73
  if (quantity <= BigInt(0))
70
74
  throw new Error(`onChain: asset ${opts.hasLabel}+${name} has zero quantity`);
71
75
  if (opts.notLabel) {
72
- const forbidden = await get(chain, `/assets/${policyId}${opts.notLabel}${nameHex}`);
73
- if (forbidden.ok) {
74
- const fq = BigInt((_b = (await forbidden.json()).quantity) !== null && _b !== void 0 ? _b : '0');
75
- if (fq > BigInt(0))
76
- throw new Error(`onChain: asset ${opts.notLabel}+${name} must NOT exist (quantity ${fq})`);
77
- }
76
+ const forbidden = await (0, blockfrost_1.blockfrostGet)(chain, `/assets/${policyId}${opts.notLabel}${nameHex}`);
77
+ const fq = BigInt((_b = forbidden === null || forbidden === void 0 ? void 0 : forbidden.quantity) !== null && _b !== void 0 ? _b : '0');
78
+ if (fq > BigInt(0))
79
+ throw new Error(`onChain: asset ${opts.notLabel}+${name} must NOT exist (quantity ${fq})`);
78
80
  }
79
81
  };
80
82
  exports.assertCip68Label = assertCip68Label;
81
83
  /** Assert an asset's total on-chain quantity is 0 (burned). */
82
84
  const assertAssetBurned = async (chain, assetUnit) => {
83
85
  var _a;
84
- const res = await get(chain, `/assets/${assetUnit}`);
85
- if (res.status === 404)
86
+ const asset = await (0, blockfrost_1.blockfrostGet)(chain, `/assets/${assetUnit}`);
87
+ if (!asset)
86
88
  return;
87
- if (!res.ok)
88
- throw new Error(`onChain: asset lookup failed (${res.status})`);
89
- const quantity = (_a = (await res.json()).quantity) !== null && _a !== void 0 ? _a : '0';
89
+ const quantity = (_a = asset.quantity) !== null && _a !== void 0 ? _a : '0';
90
90
  if (BigInt(quantity) !== BigInt(0))
91
91
  throw new Error(`onChain: expected ${assetUnit} burned, quantity is ${quantity}`);
92
92
  };
@@ -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