@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 +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/wallet/index.d.ts +56 -0
- package/wallet/index.js +54 -0
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
|
};
|
|
@@ -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;
|
package/wallet/index.js
ADDED
|
@@ -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;
|