@koralabs/kora-labs-common 6.15.3 → 6.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koralabs/kora-labs-common",
3
- "version": "6.15.3",
3
+ "version": "6.17.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
  };
@@ -0,0 +1,56 @@
1
+ /** The CIP-30 API a dApp gets from `window.cardano[key].enable()` (plus the CIP-95/CIP-103 extensions). */
2
+ export interface WalletApi {
3
+ getUtxos: () => Promise<string[] | null | undefined>;
4
+ getCollateral?: () => Promise<string[] | null>;
5
+ getExtensions?: () => Promise<{
6
+ cip: number;
7
+ }[]>;
8
+ getBalance: () => Promise<string>;
9
+ getNetworkId: () => Promise<number>;
10
+ getRewardAddresses: () => Promise<string[]>;
11
+ getChangeAddress: () => Promise<string>;
12
+ getUsedAddresses: () => Promise<string[]>;
13
+ getUnusedAddresses: () => Promise<string[]>;
14
+ signTx: (tx: string, partialSign?: boolean) => Promise<string>;
15
+ submitTx: (tx: string) => Promise<string>;
16
+ cip95?: {
17
+ getPubDRepKey: () => Promise<string>;
18
+ };
19
+ cip103?: {
20
+ signTxs: (txs: {
21
+ cbor: string;
22
+ partialSign?: boolean;
23
+ }[]) => Promise<string[]>;
24
+ submitTxs: (txs: string[]) => Promise<(string | {
25
+ error: unknown;
26
+ })[]>;
27
+ };
28
+ }
29
+ export interface WalletUtxoAsset {
30
+ policyId: string;
31
+ /** Asset name, hex (CIP-68 label included). */
32
+ hex: string;
33
+ count: number;
34
+ }
35
+ export interface ParsedUtxo {
36
+ /** The wallet's CBOR, untouched (ship this to a backend, never a re-encoding). */
37
+ cbor: string;
38
+ /** `<txHash>#<index>` */
39
+ id: string;
40
+ lovelace: bigint;
41
+ assets: WalletUtxoAsset[];
42
+ }
43
+ /**
44
+ * Parse one CIP-30 `getUtxos()` entry (`[input, output]` CBOR). Handles legacy array-form and
45
+ * Babbage map-form outputs (inline datum, reference script). Returns null for anything that is not
46
+ * a valid TransactionUnspentOutput, so one malformed entry never hides the rest of the wallet.
47
+ */
48
+ export declare const parseUtxo: (cborHex: string) => ParsedUtxo | null;
49
+ /** Lovelace in a CIP-30 `getBalance()` result (a CBOR `Value`: bare coin or `[coin, multiasset]`). Throws on malformed input. */
50
+ export declare const lovelaceFromBalance: (balanceCborHex: string) => bigint;
51
+ /**
52
+ * CIP-30 returns addresses as hex bytes. Shelley addresses (base, enterprise, pointer, reward) are
53
+ * returned in bech32 with the network's prefix; Byron addresses in base58. '' for empty input;
54
+ * throws on bytes that are not an address.
55
+ */
56
+ export declare const addressHexToBech32: (addressHex: string) => string;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addressHexToBech32 = exports.lovelaceFromBalance = exports.parseUtxo = void 0;
4
+ // Subpath module: `@koralabs/kora-labs-common/wallet`. Isomorphic (browser + Node), no network.
5
+ // Reads what a CIP-30 wallet returns (UTxO CBOR, balance CBOR, address hex) with
6
+ // @cardano-sdk/core Serialization — never a hand-rolled or generic CBOR decoder.
7
+ const core_1 = require("@cardano-sdk/core");
8
+ const POLICY_ID_HEX_LENGTH = 56;
9
+ const HexBlob = (hex) => hex;
10
+ /**
11
+ * Parse one CIP-30 `getUtxos()` entry (`[input, output]` CBOR). Handles legacy array-form and
12
+ * Babbage map-form outputs (inline datum, reference script). Returns null for anything that is not
13
+ * a valid TransactionUnspentOutput, so one malformed entry never hides the rest of the wallet.
14
+ */
15
+ const parseUtxo = (cborHex) => {
16
+ var _a;
17
+ const cbor = cborHex === null || cborHex === void 0 ? void 0 : cborHex.trim();
18
+ if (!cbor)
19
+ return null;
20
+ try {
21
+ const utxo = core_1.Serialization.TransactionUnspentOutput.fromCbor(HexBlob(cbor));
22
+ const input = utxo.input();
23
+ const value = utxo.output().amount();
24
+ const assets = [];
25
+ for (const [assetId, quantity] of (_a = value.multiasset()) !== null && _a !== void 0 ? _a : new Map()) {
26
+ assets.push({
27
+ policyId: assetId.slice(0, POLICY_ID_HEX_LENGTH),
28
+ hex: assetId.slice(POLICY_ID_HEX_LENGTH),
29
+ count: Number(quantity)
30
+ });
31
+ }
32
+ return { cbor, id: `${input.transactionId()}#${input.index()}`, lovelace: value.coin(), assets };
33
+ }
34
+ catch (_b) {
35
+ return null;
36
+ }
37
+ };
38
+ exports.parseUtxo = parseUtxo;
39
+ /** Lovelace in a CIP-30 `getBalance()` result (a CBOR `Value`: bare coin or `[coin, multiasset]`). Throws on malformed input. */
40
+ const lovelaceFromBalance = (balanceCborHex) => core_1.Serialization.Value.fromCbor(HexBlob(balanceCborHex)).coin();
41
+ exports.lovelaceFromBalance = lovelaceFromBalance;
42
+ /**
43
+ * CIP-30 returns addresses as hex bytes. Shelley addresses (base, enterprise, pointer, reward) are
44
+ * returned in bech32 with the network's prefix; Byron addresses in base58. '' for empty input;
45
+ * throws on bytes that are not an address.
46
+ */
47
+ const addressHexToBech32 = (addressHex) => {
48
+ const hex = (addressHex !== null && addressHex !== void 0 ? addressHex : '').replace(/^0x/i, '');
49
+ if (!hex)
50
+ return '';
51
+ const address = core_1.Cardano.Address.fromBytes(HexBlob(hex));
52
+ return address.getType() === core_1.Cardano.AddressType.Byron ? address.toBase58() : address.toBech32();
53
+ };
54
+ exports.addressHexToBech32 = addressHexToBech32;