@koralabs/kora-labs-common 6.16.0 → 6.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/chain/datum/cip68Metadata.d.ts +15 -0
- package/chain/datum/cip68Metadata.js +217 -0
- package/chain/failover/ChainProviderFailover.d.ts +6 -1
- package/chain/failover/ChainProviderFailover.js +24 -6
- package/chain/failover/interfaces.d.ts +33 -0
- package/chain/index.d.ts +1 -0
- package/chain/index.js +1 -0
- package/chain/providers/Blockfrost.d.ts +4 -1
- package/chain/providers/Blockfrost.js +54 -1
- package/chain/providers/Koios.d.ts +7 -2
- package/chain/providers/Koios.js +80 -11
- package/chain/transport/fetchProviderJson.d.ts +2 -0
- package/chain/transport/fetchProviderJson.js +4 -1
- package/package.json +1 -1
- package/testing/blockfrost.d.ts +12 -0
- package/testing/blockfrost.js +49 -0
- package/testing/index.d.ts +1 -0
- package/testing/index.js +1 -0
- package/testing/liveWallet.d.ts +16 -2
- package/testing/liveWallet.js +79 -61
- package/testing/onChain.d.ts +2 -6
- package/testing/onChain.js +26 -26
- package/txBuild/blockfrost.d.ts +6 -0
- package/txBuild/blockfrost.js +33 -26
- package/txBuild/scriptTx.d.ts +26 -0
- package/txBuild/scriptTx.js +43 -4
|
@@ -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
|
@@ -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;
|
package/testing/index.d.ts
CHANGED
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);
|
package/testing/liveWallet.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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>;
|
package/testing/liveWallet.js
CHANGED
|
@@ -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 =
|
|
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
|
|
76
|
+
var _a;
|
|
74
77
|
const { Cardano, Serialization, Crypto, bip39 } = await loadSdk();
|
|
75
|
-
const
|
|
78
|
+
const access = { network: config.network, blockfrostApiKey: config.blockfrostApiKey, fetchFn: config.fetchFn };
|
|
76
79
|
const networkId = config.network === 'mainnet' ? 1 : 0;
|
|
77
|
-
const
|
|
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
|
|
101
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
151
|
-
//
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
//
|
|
192
|
-
// surfaces
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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;
|
package/testing/onChain.d.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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: {
|
package/testing/onChain.js
CHANGED
|
@@ -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
|
|
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
|
|
22
|
-
if (
|
|
23
|
-
throw new Error(`onChain: GET ${path} failed (
|
|
24
|
-
return
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
66
|
-
if (!present
|
|
67
|
-
throw new Error(`onChain: expected asset ${opts.hasLabel}+${name} under ${policyId} (
|
|
68
|
-
const quantity = BigInt((_a =
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
85
|
-
if (
|
|
86
|
+
const asset = await (0, blockfrost_1.blockfrostGet)(chain, `/assets/${assetUnit}`);
|
|
87
|
+
if (!asset)
|
|
86
88
|
return;
|
|
87
|
-
|
|
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
|
};
|
package/txBuild/blockfrost.d.ts
CHANGED
|
@@ -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;
|
package/txBuild/blockfrost.js
CHANGED
|
@@ -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
|
-
|
|
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');
|
package/txBuild/scriptTx.d.ts
CHANGED
|
@@ -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
|