@koralabs/kora-labs-common 6.11.0 → 6.13.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/config.js CHANGED
@@ -26,10 +26,20 @@ const chainConfigFromEnv = (env = process.env) => {
26
26
  network: (0, providerConfig_1.normalizeNetwork)((_a = env.NETWORK) !== null && _a !== void 0 ? _a : 'mainnet'),
27
27
  koiosBearerToken: ((_b = env.KOIOS_API_BEARER_TOKEN) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
28
28
  blockfrostApiKey: ((_c = env.BLOCKFROST_API_KEY) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
29
- apiHost: ((_d = env.HANDLE_API_ENDPOINT) === null || _d === void 0 ? void 0 : _d.trim()) || undefined
29
+ apiHost: ((_d = env.HANDLE_API_ENDPOINT) === null || _d === void 0 ? void 0 : _d.trim()) || undefined,
30
+ apiHeaders: buildApiHeaders(env)
30
31
  });
31
32
  };
32
33
  exports.chainConfigFromEnv = chainConfigFromEnv;
34
+ const buildApiHeaders = (env) => {
35
+ var _a, _b;
36
+ const headers = {};
37
+ if ((_a = env.HANDLE_ME_API_KEY) === null || _a === void 0 ? void 0 : _a.trim())
38
+ headers['api-key'] = env.HANDLE_ME_API_KEY.trim();
39
+ if ((_b = env.KORA_USER_AGENT) === null || _b === void 0 ? void 0 : _b.trim())
40
+ headers['User-Agent'] = env.KORA_USER_AGENT.trim();
41
+ return Object.keys(headers).length ? headers : undefined;
42
+ };
33
43
  /**
34
44
  * Factory for the default read-failover provider set: Koios + Blockfrost, mutually redundant.
35
45
  */
@@ -1,7 +1,13 @@
1
1
  import { ICreatorDefaults } from '../../handles/interfaces';
2
- export declare const decodeDatumViaApi: <T>(apiHost: string, datum: string, schema?: Record<string, unknown>, fetcher?: (url: string, init?: any) => Promise<{
3
- text: () => Promise<string>;
4
- }>) => Promise<T>;
2
+ export interface DatumDecodeOptions {
3
+ /** Extra request headers (e.g. api-key, User-Agent) required by the Handles API /datum endpoint. */
4
+ headers?: Record<string, string>;
5
+ /** Injectable fetch for tests; defaults to the runtime global fetch. */
6
+ fetcher?: (url: string, init?: any) => Promise<{
7
+ text: () => Promise<string>;
8
+ }>;
9
+ }
10
+ export declare const decodeDatumViaApi: <T>(apiHost: string, datum: string, schema?: Record<string, unknown>, options?: DatumDecodeOptions) => Promise<T>;
5
11
  export declare const decode0xHexToUtf8: (v: unknown) => unknown;
6
12
  export declare const normalizeCreatorDefaultTextFields: (creatorDefaults: Record<string, unknown>) => void;
7
13
  export interface ImageDatumDetails {
@@ -9,6 +15,4 @@ export interface ImageDatumDetails {
9
15
  creatorDefaults?: ICreatorDefaults;
10
16
  metadata?: Record<string, unknown>;
11
17
  }
12
- export declare const getImageDataFromDatum: (apiHost: string, datum: string, fetcher?: (url: string, init?: any) => Promise<{
13
- text: () => Promise<string>;
14
- }>) => Promise<ImageDatumDetails>;
18
+ export declare const getImageDataFromDatum: (apiHost: string, datum: string, options?: DatumDecodeOptions) => Promise<ImageDatumDetails>;
@@ -6,10 +6,12 @@ exports.getImageDataFromDatum = exports.normalizeCreatorDefaultTextFields = expo
6
6
  // with a field schema so specific TEXT fields are UTF-8-decoded. This is handle-property-correctness
7
7
  // critical — the schema and the 0x-hex fallback below must not drift.
8
8
  const defaultFetch = (url, init) => globalThis.fetch(url, init);
9
- const decodeDatumViaApi = async (apiHost, datum, schema, fetcher = defaultFetch) => {
9
+ const decodeDatumViaApi = async (apiHost, datum, schema, options = {}) => {
10
+ var _a, _b;
11
+ const fetcher = (_a = options.fetcher) !== null && _a !== void 0 ? _a : defaultFetch;
10
12
  const response = await fetcher(`${apiHost}/datum?from=plutus_data_cbor&to=json`, {
11
13
  method: 'POST',
12
- headers: { 'Content-Type': 'application/json' },
14
+ headers: { 'Content-Type': 'application/json', ...((_b = options.headers) !== null && _b !== void 0 ? _b : {}) },
13
15
  body: JSON.stringify({ cbor: datum, schema })
14
16
  });
15
17
  const text = await response.text();
@@ -53,7 +55,7 @@ const normalizeCreatorDefaultTextFields = (creatorDefaults) => {
53
55
  }
54
56
  };
55
57
  exports.normalizeCreatorDefaultTextFields = normalizeCreatorDefaultTextFields;
56
- const getImageDataFromDatum = async (apiHost, datum, fetcher) => {
58
+ const getImageDataFromDatum = async (apiHost, datum, options) => {
57
59
  var _a, _b, _c, _d, _e, _f, _g, _h;
58
60
  let decodedDatum;
59
61
  try {
@@ -70,7 +72,7 @@ const getImageDataFromDatum = async (apiHost, datum, fetcher) => {
70
72
  qr_image: 'string'
71
73
  }
72
74
  }
73
- }, fetcher);
75
+ }, options);
74
76
  }
75
77
  catch (_j) {
76
78
  return { image: '' };
@@ -16,6 +16,12 @@ export interface ChainProviderConfig {
16
16
  * critical — do NOT swap it for a local CBOR decode without matching that behavior.
17
17
  */
18
18
  apiHost?: string;
19
+ /**
20
+ * Extra headers sent with the datum-decode POST to `apiHost` (e.g. `api-key`, `User-Agent`).
21
+ * Required when the Handles API gates `/datum` by api-key — omitting them silently changes
22
+ * datum-decode behavior. `chainConfigFromEnv` populates these from HANDLE_ME_API_KEY / KORA_USER_AGENT.
23
+ */
24
+ apiHeaders?: Record<string, string>;
19
25
  }
20
26
  export declare const normalizeNetwork: (network: string) => string;
21
27
  /** Public api.handle.me host for a network (mainnet has no subdomain prefix). */
@@ -118,7 +118,9 @@ class Blockfrost {
118
118
  : hex;
119
119
  const datum = await this.getAssetDatum(policyId, updateHex);
120
120
  if (datum) {
121
- return (0, imageDatum_1.getImageDataFromDatum)((_a = this.config.apiHost) !== null && _a !== void 0 ? _a : (0, providerConfig_1.defaultApiHost)(this.config.network), datum);
121
+ return (0, imageDatum_1.getImageDataFromDatum)((_a = this.config.apiHost) !== null && _a !== void 0 ? _a : (0, providerConfig_1.defaultApiHost)(this.config.network), datum, {
122
+ headers: this.config.apiHeaders
123
+ });
122
124
  }
123
125
  let image = '';
124
126
  if ((data === null || data === void 0 ? void 0 : data.onchain_metadata) && (data === null || data === void 0 ? void 0 : data.onchain_metadata.image)) {
@@ -180,7 +180,9 @@ class Koios {
180
180
  if (metadata && metadata.image)
181
181
  image = metadata.image;
182
182
  if (datum) {
183
- return (0, imageDatum_1.getImageDataFromDatum)((_a = this.config.apiHost) !== null && _a !== void 0 ? _a : (0, providerConfig_1.defaultApiHost)(this.config.network), datum);
183
+ return (0, imageDatum_1.getImageDataFromDatum)((_a = this.config.apiHost) !== null && _a !== void 0 ? _a : (0, providerConfig_1.defaultApiHost)(this.config.network), datum, {
184
+ headers: this.config.apiHeaders
185
+ });
184
186
  }
185
187
  return { image, metadata };
186
188
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koralabs/kora-labs-common",
3
- "version": "6.11.0",
3
+ "version": "6.13.0",
4
4
  "description": "Kora Labs Common Utilities",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -40,11 +40,15 @@
40
40
  "@aiken-lang/merkle-patricia-forestry": "^1.2.0",
41
41
  "@aws-sdk/client-dynamodb": "^3.549.0",
42
42
  "@aws-sdk/lib-dynamodb": "^3.549.0",
43
+ "@cardano-sdk/core": "^0.46.15",
44
+ "@cardano-sdk/crypto": "^0.4.7",
45
+ "@emurgo/cardano-message-signing-nodejs": "^1.1.0",
43
46
  "@types/jest": "^30.0.0",
44
47
  "@types/node": "^22.15.17",
45
48
  "@types/pluralize": "^0.0.29",
46
49
  "@typescript-eslint/eslint-plugin": "^8.67.0",
47
50
  "@typescript-eslint/parser": "^8.67.0",
51
+ "bip39": "^3.1.0",
48
52
  "eslint": "^8.57.1",
49
53
  "jest": "^30.4.2",
50
54
  "ts-jest": "^29.4.11",
@@ -67,11 +71,27 @@
67
71
  "brace-expansion": "5.0.9"
68
72
  },
69
73
  "peerDependencies": {
70
- "@aiken-lang/merkle-patricia-forestry": "^1.2.0"
74
+ "@aiken-lang/merkle-patricia-forestry": "^1.2.0",
75
+ "@cardano-sdk/core": "^0.46.12",
76
+ "@cardano-sdk/crypto": "^0.4.5",
77
+ "bip39": "^3.1.0",
78
+ "@emurgo/cardano-message-signing-nodejs": "^1.1.0"
71
79
  },
72
80
  "peerDependenciesMeta": {
73
81
  "@aiken-lang/merkle-patricia-forestry": {
74
82
  "optional": true
83
+ },
84
+ "@cardano-sdk/core": {
85
+ "optional": true
86
+ },
87
+ "@cardano-sdk/crypto": {
88
+ "optional": true
89
+ },
90
+ "bip39": {
91
+ "optional": true
92
+ },
93
+ "@emurgo/cardano-message-signing-nodejs": {
94
+ "optional": true
75
95
  }
76
96
  }
77
97
  }
@@ -0,0 +1,4 @@
1
+ export * from './liveWallet';
2
+ export * from './installWallet';
3
+ export * from './readOnlyGuard';
4
+ export * from './onChain';
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ // Subpath module: `@koralabs/kora-labs-common/testing`. Node-only harness for live CIP-30 browser
18
+ // journey suites. NOT re-exported from the root index. Requires the optional peer dependencies
19
+ // @cardano-sdk/core, @cardano-sdk/crypto, bip39 (and @emurgo/cardano-message-signing-nodejs for signData).
20
+ __exportStar(require("./liveWallet"), exports);
21
+ __exportStar(require("./installWallet"), exports);
22
+ __exportStar(require("./readOnlyGuard"), exports);
23
+ __exportStar(require("./onChain"), exports);
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Bridges a Node-side `LiveWallet` into browser pages as `window.cardano[walletKey]`.
3
+ *
4
+ * The bridge is exposed at BROWSER-CONTEXT level so it also exists on cross-origin pages the flow
5
+ * navigates to (e.g. auth.handle.me OAuth), not just the first origin. Typed structurally so this
6
+ * module does not depend on @playwright/test.
7
+ */
8
+ import type { LiveWallet } from './liveWallet';
9
+ export interface PageLike {
10
+ context(): {
11
+ exposeFunction(name: string, callback: (...args: any[]) => unknown): Promise<void>;
12
+ };
13
+ addInitScript(script: {
14
+ content: string;
15
+ }): Promise<void>;
16
+ }
17
+ export interface InstallWalletOptions {
18
+ walletKey?: string;
19
+ walletName?: string;
20
+ }
21
+ export declare const DEFAULT_WALLET_KEY = "handleLiveE2E";
22
+ export declare const DEFAULT_WALLET_NAME = "Handle Live E2E";
23
+ export declare const BRIDGE_FUNCTION = "__liveCip30Call";
24
+ /** Browser-side init script. Exported so it can be exercised without a browser. */
25
+ export declare const buildCip30InitScript: ({ walletKey, walletName }?: InstallWalletOptions) => string;
26
+ /** Node side of the bridge: JSON in, JSON out (so errors and bigints never cross as live objects). */
27
+ export declare const createBridgeHandler: (wallet: LiveWallet) => (method: string, paramsJson: string) => Promise<string>;
28
+ export declare const installLiveCip30Wallet: (page: PageLike, wallet: LiveWallet, options?: InstallWalletOptions) => Promise<void>;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installLiveCip30Wallet = exports.createBridgeHandler = exports.buildCip30InitScript = exports.BRIDGE_FUNCTION = exports.DEFAULT_WALLET_NAME = exports.DEFAULT_WALLET_KEY = void 0;
4
+ exports.DEFAULT_WALLET_KEY = 'handleLiveE2E';
5
+ exports.DEFAULT_WALLET_NAME = 'Handle Live E2E';
6
+ exports.BRIDGE_FUNCTION = '__liveCip30Call';
7
+ const WALLET_ICON = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Ccircle cx="12" cy="12" r="10" fill="%234da965"/%3E%3C/svg%3E';
8
+ /** Browser-side init script. Exported so it can be exercised without a browser. */
9
+ const buildCip30InitScript = ({ walletKey = exports.DEFAULT_WALLET_KEY, walletName = exports.DEFAULT_WALLET_NAME } = {}) => `
10
+ (function() {
11
+ const call = async (method, params) => {
12
+ if (typeof window.${exports.BRIDGE_FUNCTION} !== 'function') throw new Error('${exports.BRIDGE_FUNCTION} bridge not available');
13
+ return JSON.parse(await window.${exports.BRIDGE_FUNCTION}(method, JSON.stringify(params === undefined ? null : params)));
14
+ };
15
+ const api = {
16
+ getUtxos: () => call('getUtxos'),
17
+ getCollateral: (params) => call('getCollateral', params),
18
+ getBalance: () => call('getBalance'),
19
+ getNetworkId: () => call('getNetworkId'),
20
+ getRewardAddresses: () => call('getRewardAddresses'),
21
+ getChangeAddress: () => call('getChangeAddress'),
22
+ getUsedAddresses: () => call('getUsedAddresses'),
23
+ getUnusedAddresses: () => call('getUnusedAddresses'),
24
+ getExtensions: () => call('getExtensions'),
25
+ signTx: (tx) => call('signTx', tx),
26
+ signData: (addr, payload) => call('signData', { addr, payload }),
27
+ submitTx: (tx) => call('submitTx', tx),
28
+ cip95: { getPubDRepKey: () => call('getPubDRepKey') },
29
+ cip103: { signTxs: (txs) => call('signTxs', txs) }
30
+ };
31
+ const cardano = (window.cardano = window.cardano || {});
32
+ cardano[${JSON.stringify(walletKey)}] = {
33
+ name: ${JSON.stringify(walletName)},
34
+ icon: ${JSON.stringify(WALLET_ICON)},
35
+ apiVersion: '1.1.0',
36
+ supportedExtensions: [{ cip: 8 }, { cip: 95 }, { cip: 103 }],
37
+ isEnabled: async () => true,
38
+ enable: async () => api
39
+ };
40
+ })();
41
+ `;
42
+ exports.buildCip30InitScript = buildCip30InitScript;
43
+ /** Node side of the bridge: JSON in, JSON out (so errors and bigints never cross as live objects). */
44
+ const createBridgeHandler = (wallet) => async (method, paramsJson) => JSON.stringify(await wallet.call(method, JSON.parse(paramsJson)));
45
+ exports.createBridgeHandler = createBridgeHandler;
46
+ const installLiveCip30Wallet = async (page, wallet, options = {}) => {
47
+ try {
48
+ await page.context().exposeFunction(exports.BRIDGE_FUNCTION, (0, exports.createBridgeHandler)(wallet));
49
+ }
50
+ catch (error) {
51
+ // A reused context already has the binding; anything else is a real failure.
52
+ if (!/has been already registered/i.test(error.message))
53
+ throw error;
54
+ }
55
+ await page.addInitScript({ content: (0, exports.buildCip30InitScript)(options) });
56
+ };
57
+ exports.installLiveCip30Wallet = installLiveCip30Wallet;
@@ -0,0 +1,27 @@
1
+ export type LiveNetwork = 'preview' | 'preprod' | 'mainnet';
2
+ export interface LiveWalletConfig {
3
+ mnemonic: string;
4
+ network: LiveNetwork;
5
+ blockfrostApiKey: string;
6
+ /** CIP-1852 account index (m/1852'/1815'/<account>'). Separate suites should use separate accounts so they never contend for UTxOs. */
7
+ accountIndex?: number;
8
+ /** When set, every submitted signed tx is written here for post-mortem. */
9
+ submitDumpDir?: string;
10
+ /** Injectable for tests. Defaults to global fetch. */
11
+ fetchFn?: typeof fetch;
12
+ }
13
+ export interface LiveWallet {
14
+ address: string;
15
+ addressHex: string;
16
+ rewardAddress: string;
17
+ rewardAddressHex: string;
18
+ networkId: number;
19
+ paymentKeyHash: string;
20
+ stakeKeyHash: string;
21
+ dRepId: string;
22
+ call: (method: string, params?: unknown) => Promise<unknown>;
23
+ submittedTxHashes: string[];
24
+ countUtxos: () => Promise<number>;
25
+ }
26
+ export declare const blockfrostBaseUrl: (network: LiveNetwork) => string;
27
+ export declare const createLiveWallet: (config: LiveWalletConfig) => Promise<LiveWallet>;
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createLiveWallet = exports.blockfrostBaseUrl = void 0;
37
+ /**
38
+ * Live CIP-30 wallet emulator for browser journey suites (Playwright).
39
+ *
40
+ * Real keys derived from a mnemonic (CIP-1852), real UTxOs from Blockfrost, real signatures, real
41
+ * submission. It behaves like a real CIP-30 wallet on purpose:
42
+ * - getUtxos returns the FULL paginated UTxO set (no hidden/"parked" collateral),
43
+ * - getCollateral returns null (the common real-wallet case — the app must resolve collateral),
44
+ * - signTx returns a witness set holding ONLY this wallet's vkey witnesses (the app merges it),
45
+ * - signTx refuses non-canonical tx structure (hardware wallets / Eternl do).
46
+ * Runs in Node; bridge it into a page with `installLiveCip30Wallet`.
47
+ */
48
+ const blakejs_1 = require("blakejs");
49
+ const bech32_1 = require("bech32");
50
+ const tx_1 = require("../tx");
51
+ const HARDENED = 0x80000000;
52
+ let sdkPromise = null;
53
+ // @cardano-sdk/crypto ships its own nested libsodium-wrappers-sumo; it must be `ready` before any
54
+ // Bip32 operation or it throws "libsodium was not correctly initialized".
55
+ const loadSdk = () => {
56
+ sdkPromise !== null && sdkPromise !== void 0 ? sdkPromise : (sdkPromise = (async () => {
57
+ const cryptoDir = require.resolve('@cardano-sdk/crypto').replace(/\/dist\/.*/, '');
58
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
59
+ const sodium = require(require.resolve('libsodium-wrappers-sumo', { paths: [cryptoDir] }));
60
+ await sodium.ready;
61
+ const core = await Promise.resolve().then(() => __importStar(require('@cardano-sdk/core')));
62
+ // Conway set tagging (tag 258) is required for valid tx CBOR on Conway networks.
63
+ core.setInConwayEra(true);
64
+ const Crypto = await Promise.resolve().then(() => __importStar(require('@cardano-sdk/crypto')));
65
+ const bip39 = await Promise.resolve().then(() => __importStar(require('bip39')));
66
+ return { Cardano: core.Cardano, Serialization: core.Serialization, Crypto, bip39 };
67
+ })());
68
+ return sdkPromise;
69
+ };
70
+ const blockfrostBaseUrl = (network) => `https://cardano-${network}.blockfrost.io/api/v0`;
71
+ exports.blockfrostBaseUrl = blockfrostBaseUrl;
72
+ const createLiveWallet = async (config) => {
73
+ var _a, _b;
74
+ const { Cardano, Serialization, Crypto, bip39 } = await loadSdk();
75
+ const doFetch = (_a = config.fetchFn) !== null && _a !== void 0 ? _a : fetch;
76
+ 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;
79
+ const root = Crypto.Bip32PrivateKey.fromBip39Entropy(Buffer.from(bip39.mnemonicToEntropy(config.mnemonic), 'hex'), '');
80
+ const accountKey = root.derive([1852 + HARDENED, 1815 + HARDENED, account + HARDENED]);
81
+ const paymentKey = accountKey.derive([0, 0]).toRawKey();
82
+ const stakeKey = accountKey.derive([2, 0]).toRawKey();
83
+ const paymentKeyHash = paymentKey.toPublic().hash().hex();
84
+ const stakeKeyHash = stakeKey.toPublic().hash().hex();
85
+ const paymentCredential = { type: Cardano.CredentialType.KeyHash, hash: paymentKeyHash };
86
+ const stakeCredential = { type: Cardano.CredentialType.KeyHash, hash: stakeKeyHash };
87
+ const baseAddress = Cardano.BaseAddress.fromCredentials(networkId, paymentCredential, stakeCredential).toAddress();
88
+ const address = baseAddress.toBech32();
89
+ const addressHex = baseAddress.toBytes();
90
+ // Some handle.me flows park tokens at EnterpriseAddress(stakeCredential); a real wallet owning
91
+ // the stake key reports those UTxOs too.
92
+ const enterpriseStakeAddress = Cardano.EnterpriseAddress.fromCredentials(networkId, stakeCredential).toAddress().toBech32();
93
+ const rewardAddress = Cardano.RewardAccount.fromCredential(stakeCredential, networkId);
94
+ const rewardAddressHex = Cardano.Address.fromString(rewardAddress).toBytes();
95
+ // CIP-95: DRep id = bech32('drep', 0x22 || blake2b-224(pubkey)).
96
+ const dRepPubKeyHex = stakeKey.toPublic().hex();
97
+ const dRepKeyHash = (0, blakejs_1.blake2b)(Buffer.from(dRepPubKeyHex, 'hex'), undefined, 28);
98
+ const dRepId = bech32_1.bech32.encode('drep', bech32_1.bech32.toWords(Uint8Array.from([0x22, ...dRepKeyHash])), 128);
99
+ 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
+ };
106
+ const fetchAddressUtxos = async (addr) => {
107
+ const all = [];
108
+ 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)
114
+ 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
+ for (const u of batch)
119
+ u._sourceAddr = addr;
120
+ all.push(...batch);
121
+ if (batch.length < 100)
122
+ break;
123
+ }
124
+ return all;
125
+ };
126
+ const utxoToCborHex = (u) => {
127
+ var _a, _b, _c;
128
+ const coins = BigInt((_b = (_a = u.amount.find((a) => a.unit === 'lovelace')) === null || _a === void 0 ? void 0 : _a.quantity) !== null && _b !== void 0 ? _b : '0');
129
+ const assets = new Map();
130
+ for (const a of u.amount)
131
+ if (a.unit !== 'lovelace')
132
+ assets.set(a.unit, BigInt(a.quantity));
133
+ const output = {
134
+ address: (_c = u._sourceAddr) !== null && _c !== void 0 ? _c : address,
135
+ value: { coins, ...(assets.size > 0 ? { assets } : {}) },
136
+ ...(u.inline_datum ? { datum: Serialization.PlutusData.fromCbor(u.inline_datum).toCore() } : {}),
137
+ ...(!u.inline_datum && u.data_hash ? { datumHash: u.data_hash } : {})
138
+ };
139
+ return new Serialization.TransactionUnspentOutput(Serialization.TransactionInput.fromCore({ txId: u.tx_hash, index: u.output_index }), Serialization.TransactionOutput.fromCore(output)).toCbor();
140
+ };
141
+ const getUtxos = async () => {
142
+ const utxos = [...(await fetchAddressUtxos(address)), ...(await fetchAddressUtxos(enterpriseStakeAddress))];
143
+ return utxos.map(utxoToCborHex);
144
+ };
145
+ 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));
148
+ return Serialization.Value.fromCore({ coins }).toCbor();
149
+ };
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)).
152
+ const needsStakeKeySignature = async (body) => {
153
+ var _a, _b, _c, _d;
154
+ if (((_a = body.requiredExtraSignatures) !== null && _a !== void 0 ? _a : []).some((s) => s === stakeKeyHash))
155
+ return true;
156
+ 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;
165
+ };
166
+ const signTx = async (txCborHex) => {
167
+ (0, tx_1.assertCanonicalCbor)(txCborHex, 'signTx');
168
+ const tx = Serialization.Transaction.fromCbor(txCborHex);
169
+ const bodyHash = tx.body().hash();
170
+ const signatures = new Map();
171
+ signatures.set(paymentKey.toPublic().hex(), paymentKey.sign(bodyHash).hex());
172
+ if (await needsStakeKeySignature(tx.body().toCore())) {
173
+ signatures.set(stakeKey.toPublic().hex(), stakeKey.sign(bodyHash).hex());
174
+ }
175
+ return Serialization.TransactionWitnessSet.fromCore({ signatures }).toCbor();
176
+ };
177
+ const signTxs = async (txs) => {
178
+ var _a;
179
+ const out = [];
180
+ for (const entry of txs)
181
+ out.push(await signTx(typeof entry === 'string' ? entry : ((_a = entry.cbor) !== null && _a !== void 0 ? _a : entry.tx)));
182
+ return out;
183
+ };
184
+ const submitTx = async (signedTxCbor) => {
185
+ if (config.submitDumpDir) {
186
+ const fs = await Promise.resolve().then(() => __importStar(require('node:fs')));
187
+ const path = await Promise.resolve().then(() => __importStar(require('node:path')));
188
+ fs.mkdirSync(config.submitDumpDir, { recursive: true });
189
+ fs.writeFileSync(path.join(config.submitDumpDir, `submit-${Date.now()}.cbor.hex`), signedTxCbor);
190
+ }
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}`);
214
+ };
215
+ // CIP-8 COSE_Sign1 over the payload with the payment key (CIP-30 signData).
216
+ const signData = async (addrHex, payloadHex) => {
217
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
218
+ const cms = require('@emurgo/cardano-message-signing-nodejs');
219
+ const protectedHeaders = cms.HeaderMap.new();
220
+ protectedHeaders.set_algorithm_id(cms.Label.from_algorithm_id(cms.AlgorithmId.EdDSA));
221
+ protectedHeaders.set_header(cms.Label.new_text('address'), cms.CBORValue.new_bytes(Buffer.from(addrHex, 'hex')));
222
+ const headers = cms.Headers.new(cms.ProtectedHeaderMap.new(protectedHeaders), cms.HeaderMap.new());
223
+ const builder = cms.COSESign1Builder.new(headers, Buffer.from(payloadHex, 'hex'), false);
224
+ const toSign = Buffer.from(builder.make_data_to_sign().to_bytes()).toString('hex');
225
+ const coseSign1 = builder.build(Buffer.from(paymentKey.sign(toSign).hex(), 'hex'));
226
+ const coseKey = cms.COSEKey.new(cms.Label.from_key_type(cms.KeyType.OKP));
227
+ coseKey.set_algorithm_id(cms.Label.from_algorithm_id(cms.AlgorithmId.EdDSA));
228
+ coseKey.set_header(cms.Label.new_int(cms.Int.new_i32(-1)), cms.CBORValue.from_label(cms.Label.from_curve_type(cms.CurveType.Ed25519)));
229
+ coseKey.set_header(cms.Label.new_int(cms.Int.new_i32(-2)), cms.CBORValue.new_bytes(Buffer.from(paymentKey.toPublic().hex(), 'hex')));
230
+ return {
231
+ signature: Buffer.from(coseSign1.to_bytes()).toString('hex'),
232
+ key: Buffer.from(coseKey.to_bytes()).toString('hex')
233
+ };
234
+ };
235
+ const call = async (method, params) => {
236
+ switch (method) {
237
+ case 'getUtxos':
238
+ return getUtxos();
239
+ case 'getCollateral':
240
+ return null;
241
+ case 'getBalance':
242
+ return getBalance();
243
+ case 'getNetworkId':
244
+ return networkId;
245
+ case 'getRewardAddresses':
246
+ return [rewardAddressHex];
247
+ case 'getChangeAddress':
248
+ return addressHex;
249
+ case 'getUsedAddresses':
250
+ return [addressHex];
251
+ case 'getUnusedAddresses':
252
+ return [];
253
+ case 'getExtensions':
254
+ return [{ cip: 8 }, { cip: 95 }, { cip: 103 }];
255
+ case 'signTx':
256
+ return signTx(params);
257
+ case 'signTxs':
258
+ return signTxs(params);
259
+ case 'signData': {
260
+ const p = params;
261
+ return signData(p.addr, p.payload);
262
+ }
263
+ case 'submitTx':
264
+ return submitTx(params);
265
+ case 'getPubDRepKey':
266
+ return dRepPubKeyHex;
267
+ default:
268
+ throw new Error(`Unknown CIP-30 method: ${method}`);
269
+ }
270
+ };
271
+ return {
272
+ address,
273
+ addressHex,
274
+ rewardAddress,
275
+ rewardAddressHex,
276
+ networkId,
277
+ paymentKeyHash,
278
+ stakeKeyHash,
279
+ dRepId,
280
+ call,
281
+ submittedTxHashes,
282
+ countUtxos: async () => (await fetchAddressUtxos(address)).length
283
+ };
284
+ };
285
+ exports.createLiveWallet = createLiveWallet;
@@ -0,0 +1,58 @@
1
+ import { LiveNetwork } from './liveWallet';
2
+ export declare const LBL_222 = "000de140";
3
+ export declare const LBL_100 = "000643b0";
4
+ export declare const LBL_000 = "00000000";
5
+ export declare const LBL_444 = "001bc280";
6
+ export declare const hexOf: (s: string) => string;
7
+ export interface ChainConfig {
8
+ network: LiveNetwork;
9
+ blockfrostApiKey: string;
10
+ fetchFn?: typeof fetch;
11
+ }
12
+ export interface TxUtxoOutput {
13
+ address: string;
14
+ amount: {
15
+ unit: string;
16
+ quantity: string;
17
+ }[];
18
+ output_index: number;
19
+ inline_datum?: string | null;
20
+ data_hash?: string | null;
21
+ consumed_by_tx?: string | null;
22
+ }
23
+ export interface TxUtxos {
24
+ hash: string;
25
+ inputs: (TxUtxoOutput & {
26
+ tx_hash: string;
27
+ })[];
28
+ outputs: TxUtxoOutput[];
29
+ }
30
+ export declare const fetchTxUtxos: (chain: ChainConfig, txHash: string) => Promise<TxUtxos>;
31
+ export declare const lovelaceOf: (amount: {
32
+ unit: string;
33
+ quantity: string;
34
+ }[]) => bigint;
35
+ export interface TxConfirmation {
36
+ confirmed: boolean;
37
+ block?: string;
38
+ validContract?: boolean;
39
+ }
40
+ /** Poll until the tx is in a block. A tx in a block with `valid_contract: false` is NOT a success. */
41
+ export declare const waitForTxConfirmation: (chain: ChainConfig, txHash: string, timeoutMs?: number, pollMs?: number) => Promise<TxConfirmation>;
42
+ /** Poll a tx output until it is spent; returns the spending tx hash, or null on timeout. */
43
+ export declare const waitForOutputConsumed: (chain: ChainConfig, txHash: string, outputIndex: number, timeoutMs: number, pollMs?: number) => Promise<string | null>;
44
+ /** Assert `<policy><label><name>` exists with quantity > 0 and, optionally, that another label does NOT exist. */
45
+ export declare const assertCip68Label: (chain: ChainConfig, policyId: string, name: string, opts: {
46
+ hasLabel: string;
47
+ notLabel?: string;
48
+ }) => Promise<void>;
49
+ /** Assert an asset's total on-chain quantity is 0 (burned). */
50
+ export declare const assertAssetBurned: (chain: ChainConfig, assetUnit: string) => Promise<void>;
51
+ /** Assert a tx paid at least `minLovelace` (summed over outputs) to `toAddress`. */
52
+ export declare const assertAdaTransfer: (chain: ChainConfig, txHash: string, toAddress: string, minLovelace: bigint) => Promise<bigint>;
53
+ /** Resolve an output's datum CBOR hex (inline, or by hash). */
54
+ export declare const fetchOutputDatumCbor: (chain: ChainConfig, output: TxUtxoOutput) => Promise<string>;
55
+ /** Decode the CIP-68 `extra` map (constr field 2) from the output of `txHash` that holds `<label><name>`. */
56
+ export declare const fetchCip68ExtraFromTx: (chain: ChainConfig, txHash: string, name: string, label?: string) => Promise<Map<Buffer, unknown>>;
57
+ /** Read a scalar field from a decoded CIP-68 map (metadata or extra). */
58
+ export declare const cip68Field: (map: Map<Buffer, unknown>, key: string) => string | null;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.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
+ /**
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.
10
+ */
11
+ const cbor_1 = __importDefault(require("cbor"));
12
+ const liveWallet_1 = require("./liveWallet");
13
+ exports.LBL_222 = '000de140'; // CIP-68 user/owner NFT
14
+ exports.LBL_100 = '000643b0'; // CIP-68 reference token
15
+ exports.LBL_000 = '00000000'; // virtual subhandle token
16
+ exports.LBL_444 = '001bc280'; // CIP-68 RFT
17
+ const hexOf = (s) => Buffer.from(s, 'utf8').toString('hex');
18
+ 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
+ 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());
25
+ };
26
+ const fetchTxUtxos = (chain, txHash) => getJson(chain, `/txs/${txHash}/utxos`);
27
+ exports.fetchTxUtxos = fetchTxUtxos;
28
+ const lovelaceOf = (amount) => { var _a, _b; return BigInt((_b = (_a = amount.find((a) => a.unit === 'lovelace')) === null || _a === void 0 ? void 0 : _a.quantity) !== null && _b !== void 0 ? _b : '0'); };
29
+ exports.lovelaceOf = lovelaceOf;
30
+ /** Poll until the tx is in a block. A tx in a block with `valid_contract: false` is NOT a success. */
31
+ const waitForTxConfirmation = async (chain, txHash, timeoutMs = 180000, pollMs = 5000) => {
32
+ const start = Date.now();
33
+ 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());
37
+ return { confirmed: true, block: data.block, validContract: data.valid_contract };
38
+ }
39
+ if (Date.now() - start >= timeoutMs)
40
+ return { confirmed: false };
41
+ await new Promise((r) => setTimeout(r, pollMs));
42
+ }
43
+ };
44
+ exports.waitForTxConfirmation = waitForTxConfirmation;
45
+ /** Poll a tx output until it is spent; returns the spending tx hash, or null on timeout. */
46
+ const waitForOutputConsumed = async (chain, txHash, outputIndex, timeoutMs, pollMs = 15000) => {
47
+ const start = Date.now();
48
+ for (;;) {
49
+ const utxos = await (0, exports.fetchTxUtxos)(chain, txHash);
50
+ const out = utxos.outputs.find((o) => o.output_index === outputIndex);
51
+ if (!out)
52
+ throw new Error(`onChain: ${txHash}#${outputIndex} does not exist`);
53
+ if (out.consumed_by_tx)
54
+ return out.consumed_by_tx;
55
+ if (Date.now() - start >= timeoutMs)
56
+ return null;
57
+ await new Promise((r) => setTimeout(r, pollMs));
58
+ }
59
+ };
60
+ exports.waitForOutputConsumed = waitForOutputConsumed;
61
+ /** Assert `<policy><label><name>` exists with quantity > 0 and, optionally, that another label does NOT exist. */
62
+ const assertCip68Label = async (chain, policyId, name, opts) => {
63
+ var _a, _b;
64
+ 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
+ if (quantity <= BigInt(0))
70
+ throw new Error(`onChain: asset ${opts.hasLabel}+${name} has zero quantity`);
71
+ 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
+ }
78
+ }
79
+ };
80
+ exports.assertCip68Label = assertCip68Label;
81
+ /** Assert an asset's total on-chain quantity is 0 (burned). */
82
+ const assertAssetBurned = async (chain, assetUnit) => {
83
+ var _a;
84
+ const res = await get(chain, `/assets/${assetUnit}`);
85
+ if (res.status === 404)
86
+ 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';
90
+ if (BigInt(quantity) !== BigInt(0))
91
+ throw new Error(`onChain: expected ${assetUnit} burned, quantity is ${quantity}`);
92
+ };
93
+ exports.assertAssetBurned = assertAssetBurned;
94
+ /** Assert a tx paid at least `minLovelace` (summed over outputs) to `toAddress`. */
95
+ const assertAdaTransfer = async (chain, txHash, toAddress, minLovelace) => {
96
+ const { outputs } = await (0, exports.fetchTxUtxos)(chain, txHash);
97
+ const paid = outputs.filter((o) => o.address === toAddress).reduce((sum, o) => sum + (0, exports.lovelaceOf)(o.amount), BigInt(0));
98
+ if (paid < minLovelace)
99
+ throw new Error(`onChain: tx ${txHash} paid ${paid} lovelace to ${toAddress}, expected >= ${minLovelace}`);
100
+ return paid;
101
+ };
102
+ exports.assertAdaTransfer = assertAdaTransfer;
103
+ /** Resolve an output's datum CBOR hex (inline, or by hash). */
104
+ const fetchOutputDatumCbor = async (chain, output) => {
105
+ if (output.inline_datum)
106
+ return output.inline_datum;
107
+ if (output.data_hash)
108
+ return (await getJson(chain, `/scripts/datum/${output.data_hash}/cbor`)).cbor;
109
+ throw new Error(`onChain: output #${output.output_index} carries no datum`);
110
+ };
111
+ exports.fetchOutputDatumCbor = fetchOutputDatumCbor;
112
+ /** Decode the CIP-68 `extra` map (constr field 2) from the output of `txHash` that holds `<label><name>`. */
113
+ const fetchCip68ExtraFromTx = async (chain, txHash, name, label = exports.LBL_100) => {
114
+ var _a;
115
+ const { outputs } = await (0, exports.fetchTxUtxos)(chain, txHash);
116
+ const suffix = `${label}${(0, exports.hexOf)(name)}`;
117
+ const out = outputs.find((o) => o.amount.some((a) => a.unit.endsWith(suffix)));
118
+ if (!out)
119
+ throw new Error(`onChain: no ${label} output for ${name} in ${txHash}`);
120
+ const decoded = cbor_1.default.decodeFirstSync(Buffer.from(await (0, exports.fetchOutputDatumCbor)(chain, out), 'hex'));
121
+ const extra = (_a = decoded.value) === null || _a === void 0 ? void 0 : _a[2];
122
+ if (!(extra instanceof Map))
123
+ throw new Error('onChain: CIP-68 datum has no extra map');
124
+ return extra;
125
+ };
126
+ exports.fetchCip68ExtraFromTx = fetchCip68ExtraFromTx;
127
+ /** Read a scalar field from a decoded CIP-68 map (metadata or extra). */
128
+ const cip68Field = (map, key) => {
129
+ for (const [k, v] of map.entries()) {
130
+ if (!Buffer.isBuffer(k) || k.toString('utf8') !== key)
131
+ continue;
132
+ if (Buffer.isBuffer(v))
133
+ return v.toString('utf8');
134
+ if (v == null)
135
+ return null;
136
+ if (v instanceof Map || Array.isArray(v))
137
+ return JSON.stringify(v);
138
+ return String(v);
139
+ }
140
+ return null;
141
+ };
142
+ exports.cip68Field = cip68Field;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Read-only wallet guard for MAINNET smoke suites.
3
+ *
4
+ * Wraps a real LiveWallet so it is physically unable to move funds: signTx / signTxs / submitTx
5
+ * are recorded and THROWN. Everything else (getUtxos, signData for OAuth, ...) passes through.
6
+ * A scope proves it reached the transaction by asserting `reachedSign()` — the app built a tx and
7
+ * asked to sign it — and stops there.
8
+ */
9
+ import type { LiveWallet } from './liveWallet';
10
+ export declare const FUND_MOVING_METHODS: readonly ["signTx", "signTxs", "submitTx"];
11
+ export declare const READ_ONLY_GUARD_ERROR = "E2E_READONLY_GUARD";
12
+ export interface SignRequest {
13
+ method: (typeof FUND_MOVING_METHODS)[number];
14
+ at: number;
15
+ /** The exact tx CBOR the app asked to sign (first tx for signTxs). Decode it to assert what would have been signed. */
16
+ txCbor: string;
17
+ }
18
+ export interface ReadOnlyWallet {
19
+ wallet: LiveWallet;
20
+ signRequests: SignRequest[];
21
+ reachedSign(): boolean;
22
+ reset(): void;
23
+ waitForSignRequest(timeoutMs: number, tick?: () => Promise<unknown>): Promise<boolean>;
24
+ }
25
+ export declare const makeReadOnlyWallet: (inner: LiveWallet) => ReadOnlyWallet;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeReadOnlyWallet = exports.READ_ONLY_GUARD_ERROR = exports.FUND_MOVING_METHODS = void 0;
4
+ exports.FUND_MOVING_METHODS = ['signTx', 'signTxs', 'submitTx'];
5
+ exports.READ_ONLY_GUARD_ERROR = 'E2E_READONLY_GUARD';
6
+ const firstTxCbor = (params) => {
7
+ var _a, _b;
8
+ if (typeof params === 'string')
9
+ return params;
10
+ if (Array.isArray(params)) {
11
+ const first = params[0];
12
+ return typeof first === 'string' ? first : ((_b = (_a = first === null || first === void 0 ? void 0 : first.cbor) !== null && _a !== void 0 ? _a : first === null || first === void 0 ? void 0 : first.tx) !== null && _b !== void 0 ? _b : '');
13
+ }
14
+ return '';
15
+ };
16
+ const makeReadOnlyWallet = (inner) => {
17
+ const signRequests = [];
18
+ const guardedCall = async (method, params) => {
19
+ if (exports.FUND_MOVING_METHODS.includes(method)) {
20
+ signRequests.push({ method: method, at: Date.now(), txCbor: firstTxCbor(params) });
21
+ throw new Error(`${exports.READ_ONLY_GUARD_ERROR}: '${method}' is blocked — this suite reaches the transaction and stops; it never signs or submits.`);
22
+ }
23
+ return inner.call(method, params);
24
+ };
25
+ return {
26
+ wallet: { ...inner, call: guardedCall },
27
+ signRequests,
28
+ reachedSign: () => signRequests.length > 0,
29
+ reset: () => {
30
+ signRequests.length = 0;
31
+ },
32
+ waitForSignRequest: async (timeoutMs, tick) => {
33
+ const start = Date.now();
34
+ while (signRequests.length === 0 && Date.now() - start < timeoutMs) {
35
+ if (tick)
36
+ await tick();
37
+ await new Promise((r) => setTimeout(r, 1500));
38
+ }
39
+ return signRequests.length > 0;
40
+ }
41
+ };
42
+ };
43
+ exports.makeReadOnlyWallet = makeReadOnlyWallet;
package/tx/cbor.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ export interface CborHeader {
2
+ majorType: number;
3
+ additionalInfo: number;
4
+ length: number;
5
+ headerEnd: number;
6
+ }
7
+ export declare const readCborHeader: (buf: Uint8Array, start: number) => CborHeader;
8
+ /** Return the offset immediately after the CBOR item starting at `start`. */
9
+ export declare const skipCborItem: (buf: Uint8Array, start: number) => number;
10
+ /** Byte span of the witness set (element 1) inside `transaction = [body, witness_set, is_valid, aux]`. */
11
+ export declare const locateWitnessSet: (txBytes: Uint8Array) => {
12
+ start: number;
13
+ end: number;
14
+ };
15
+ /** Byte span of the transaction body (element 0). The tx id is blake2b-256 over exactly these bytes. */
16
+ export declare const locateTxBody: (txBytes: Uint8Array) => {
17
+ start: number;
18
+ end: number;
19
+ };
20
+ /**
21
+ * Merge a CIP-30 `signTx(tx, partialSign=true)` witness set into the unsigned tx WITHOUT
22
+ * re-encoding the body or any non-vkey witness entry (scripts, redeemers, datums stay
23
+ * byte-identical). Existing real vkey witnesses (e.g. a backend policy pre-sign) are preserved;
24
+ * fee-estimation placeholders are dropped.
25
+ */
26
+ export declare const mergeWitnessSet: (unsignedTxHex: string, walletWitnessSetHex: string) => string;
27
+ export interface CanonicalViolation {
28
+ path: string;
29
+ detail: string;
30
+ }
31
+ /**
32
+ * Canonical-CBOR check (CIP-21 key order) for the transaction-structure maps only: the body map
33
+ * (tx[0]) and the witness map (tx[1]). Hardware wallets and Eternl reject a tx whose structure maps
34
+ * are out of canonical order. Plutus data and multi-asset maps are deliberately NOT checked — they
35
+ * are hashed as-is and canonicalizing them would change hashes.
36
+ */
37
+ export declare const findCanonicalCborViolations: (txHex: string) => CanonicalViolation[];
38
+ export declare const assertCanonicalCbor: (txHex: string, label?: string) => void;
package/tx/cbor.js ADDED
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+ // Byte-level CBOR helpers for signed-transaction assembly. Pure functions, no deps beyond Buffer,
3
+ // so they are safe in the browser (wallet contexts) and in Node (test harnesses).
4
+ //
5
+ // Why byte-level: decoding a built tx into an SDK object and re-encoding it can change bytes
6
+ // (definite vs indefinite arrays, integer widths, key order). The node hashes the exact bytes it
7
+ // receives, so any re-encode after the backend computed script_data_hash / the body hash breaks
8
+ // the tx (PPViewHashesDontMatch, or a signature over a body that no longer exists).
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.assertCanonicalCbor = exports.findCanonicalCborViolations = exports.mergeWitnessSet = exports.locateTxBody = exports.locateWitnessSet = exports.skipCborItem = exports.readCborHeader = void 0;
11
+ const readCborHeader = (buf, start) => {
12
+ const majorType = buf[start] >> 5;
13
+ const additionalInfo = buf[start] & 0x1f;
14
+ let headerEnd = start + 1;
15
+ let length = additionalInfo;
16
+ if (additionalInfo === 24) {
17
+ length = buf[start + 1];
18
+ headerEnd = start + 2;
19
+ }
20
+ else if (additionalInfo === 25) {
21
+ length = (buf[start + 1] << 8) | buf[start + 2];
22
+ headerEnd = start + 3;
23
+ }
24
+ else if (additionalInfo === 26) {
25
+ length = buf[start + 1] * 0x1000000 + ((buf[start + 2] << 16) | (buf[start + 3] << 8) | buf[start + 4]);
26
+ headerEnd = start + 5;
27
+ }
28
+ else if (additionalInfo === 27) {
29
+ let v = 0;
30
+ for (let i = 0; i < 8; i++)
31
+ v = v * 256 + buf[start + 1 + i];
32
+ length = v;
33
+ headerEnd = start + 9;
34
+ }
35
+ else if (additionalInfo >= 28 && additionalInfo <= 30) {
36
+ throw new Error(`Unsupported CBOR additional info ${additionalInfo} at byte ${start}`);
37
+ }
38
+ return { majorType, additionalInfo, length, headerEnd };
39
+ };
40
+ exports.readCborHeader = readCborHeader;
41
+ /** Return the offset immediately after the CBOR item starting at `start`. */
42
+ const skipCborItem = (buf, start) => {
43
+ const { majorType, additionalInfo, length, headerEnd } = (0, exports.readCborHeader)(buf, start);
44
+ if (additionalInfo === 31) {
45
+ let c = headerEnd;
46
+ while (buf[c] !== 0xff) {
47
+ c = (0, exports.skipCborItem)(buf, c);
48
+ if (majorType === 5)
49
+ c = (0, exports.skipCborItem)(buf, c);
50
+ }
51
+ return c + 1;
52
+ }
53
+ if (majorType <= 1 || majorType === 7)
54
+ return headerEnd;
55
+ if (majorType === 2 || majorType === 3)
56
+ return headerEnd + length;
57
+ if (majorType === 6)
58
+ return (0, exports.skipCborItem)(buf, headerEnd);
59
+ let c = headerEnd;
60
+ const items = majorType === 5 ? length * 2 : length;
61
+ for (let i = 0; i < items; i++)
62
+ c = (0, exports.skipCborItem)(buf, c);
63
+ return c;
64
+ };
65
+ exports.skipCborItem = skipCborItem;
66
+ /** Byte span of the witness set (element 1) inside `transaction = [body, witness_set, is_valid, aux]`. */
67
+ const locateWitnessSet = (txBytes) => {
68
+ const outer = (0, exports.readCborHeader)(txBytes, 0);
69
+ if (outer.majorType !== 4)
70
+ throw new Error('Transaction CBOR is not an array');
71
+ const start = (0, exports.skipCborItem)(txBytes, outer.headerEnd);
72
+ return { start, end: (0, exports.skipCborItem)(txBytes, start) };
73
+ };
74
+ exports.locateWitnessSet = locateWitnessSet;
75
+ /** Byte span of the transaction body (element 0). The tx id is blake2b-256 over exactly these bytes. */
76
+ const locateTxBody = (txBytes) => {
77
+ const outer = (0, exports.readCborHeader)(txBytes, 0);
78
+ if (outer.majorType !== 4)
79
+ throw new Error('Transaction CBOR is not an array');
80
+ return { start: outer.headerEnd, end: (0, exports.skipCborItem)(txBytes, outer.headerEnd) };
81
+ };
82
+ exports.locateTxBody = locateTxBody;
83
+ const encodeUintHeader = (majorType, value) => {
84
+ const tag = majorType << 5;
85
+ if (value < 24)
86
+ return Buffer.from([tag | value]);
87
+ if (value < 0x100)
88
+ return Buffer.from([tag | 24, value]);
89
+ if (value < 0x10000)
90
+ return Buffer.from([tag | 25, (value >> 8) & 0xff, value & 0xff]);
91
+ return Buffer.from([tag | 26, (value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff]);
92
+ };
93
+ const parseMapEntries = (buf, mapStart) => {
94
+ const hdr = (0, exports.readCborHeader)(buf, mapStart);
95
+ if (hdr.majorType !== 5)
96
+ throw new Error('Expected CBOR map for witness set');
97
+ if (hdr.additionalInfo === 31)
98
+ throw new Error('Indefinite-length witness-set map not supported');
99
+ const entries = [];
100
+ let off = hdr.headerEnd;
101
+ for (let i = 0; i < hdr.length; i++) {
102
+ const keyEnd = (0, exports.skipCborItem)(buf, off);
103
+ const valueEnd = (0, exports.skipCborItem)(buf, keyEnd);
104
+ entries.push({ keyStart: off, keyEnd, valueStart: keyEnd, valueEnd });
105
+ off = valueEnd;
106
+ }
107
+ return entries;
108
+ };
109
+ const findUintKey = (entries, buf, key) => entries.find((e) => {
110
+ const hdr = (0, exports.readCborHeader)(buf, e.keyStart);
111
+ return hdr.majorType === 0 && hdr.length === key && hdr.headerEnd === e.keyEnd;
112
+ });
113
+ // `tag(258, array)` (Conway set) or a bare array → element count + raw element bytes.
114
+ const splitVkeyArray = (buf, valueStart, valueEnd) => {
115
+ let off = valueStart;
116
+ const first = (0, exports.readCborHeader)(buf, off);
117
+ if (first.majorType === 6 && first.length === 258)
118
+ off = first.headerEnd;
119
+ const arr = (0, exports.readCborHeader)(buf, off);
120
+ if (arr.majorType !== 4)
121
+ throw new Error('Expected vkey-witness array');
122
+ if (arr.additionalInfo === 31)
123
+ throw new Error('Indefinite-length vkey-witness array not supported');
124
+ return { count: arr.length, itemsBytes: Buffer.from(buf.slice(arr.headerEnd, valueEnd)) };
125
+ };
126
+ // Backends size fees with placeholder vkeys whose pubkeys are (almost) all zero. Those must be
127
+ // replaced by real signatures, never kept alongside them. A real ed25519 pubkey never begins with
128
+ // 28+ zero bytes.
129
+ const dropPlaceholderVkeys = (itemsBytes, count) => {
130
+ const kept = [];
131
+ let off = 0;
132
+ for (let i = 0; i < count; i++) {
133
+ const itemStart = off;
134
+ const arr = (0, exports.readCborHeader)(itemsBytes, off);
135
+ const pub = (0, exports.readCborHeader)(itemsBytes, arr.headerEnd);
136
+ const pubBytes = itemsBytes.subarray(pub.headerEnd, pub.headerEnd + pub.length);
137
+ off = (0, exports.skipCborItem)(itemsBytes, (0, exports.skipCborItem)(itemsBytes, arr.headerEnd));
138
+ let leadingZeros = 0;
139
+ for (const b of pubBytes) {
140
+ if (b !== 0)
141
+ break;
142
+ leadingZeros++;
143
+ }
144
+ if (leadingZeros < 28)
145
+ kept.push(itemsBytes.subarray(itemStart, off));
146
+ }
147
+ return { itemsBytes: Buffer.concat(kept), count: kept.length };
148
+ };
149
+ /**
150
+ * Merge a CIP-30 `signTx(tx, partialSign=true)` witness set into the unsigned tx WITHOUT
151
+ * re-encoding the body or any non-vkey witness entry (scripts, redeemers, datums stay
152
+ * byte-identical). Existing real vkey witnesses (e.g. a backend policy pre-sign) are preserved;
153
+ * fee-estimation placeholders are dropped.
154
+ */
155
+ const mergeWitnessSet = (unsignedTxHex, walletWitnessSetHex) => {
156
+ const txBytes = Buffer.from(unsignedTxHex, 'hex');
157
+ const walletBytes = Buffer.from(walletWitnessSetHex, 'hex');
158
+ const { start: wsStart, end: wsEnd } = (0, exports.locateWitnessSet)(txBytes);
159
+ const existing = parseMapEntries(txBytes, wsStart);
160
+ const wallet = parseMapEntries(walletBytes, 0);
161
+ const existingVkey = findUintKey(existing, txBytes, 0);
162
+ const walletVkey = findUintKey(wallet, walletBytes, 0);
163
+ if (!walletVkey)
164
+ return unsignedTxHex;
165
+ let mergedVkeyValue;
166
+ if (existingVkey) {
167
+ const ours = splitVkeyArray(txBytes, existingVkey.valueStart, existingVkey.valueEnd);
168
+ const kept = dropPlaceholderVkeys(ours.itemsBytes, ours.count);
169
+ const theirs = splitVkeyArray(walletBytes, walletVkey.valueStart, walletVkey.valueEnd);
170
+ mergedVkeyValue = Buffer.concat([
171
+ Buffer.from([0xd9, 0x01, 0x02]),
172
+ encodeUintHeader(4, kept.count + theirs.count),
173
+ kept.itemsBytes,
174
+ theirs.itemsBytes
175
+ ]);
176
+ }
177
+ else {
178
+ mergedVkeyValue = Buffer.from(walletBytes.subarray(walletVkey.valueStart, walletVkey.valueEnd));
179
+ }
180
+ // Key 0 first keeps the witness map canonical; every other entry is copied byte-for-byte.
181
+ const others = existing.filter((e) => e !== existingVkey);
182
+ const newWitnessSet = Buffer.concat([
183
+ encodeUintHeader(5, others.length + 1),
184
+ Buffer.from([0x00]),
185
+ mergedVkeyValue,
186
+ ...others.map((e) => txBytes.subarray(e.keyStart, e.valueEnd))
187
+ ]);
188
+ return Buffer.concat([txBytes.subarray(0, wsStart), newWitnessSet, txBytes.subarray(wsEnd)]).toString('hex');
189
+ };
190
+ exports.mergeWitnessSet = mergeWitnessSet;
191
+ const compareEncodedKeys = (a, b) => (a.length !== b.length ? a.length - b.length : Buffer.compare(a, b));
192
+ /**
193
+ * Canonical-CBOR check (CIP-21 key order) for the transaction-structure maps only: the body map
194
+ * (tx[0]) and the witness map (tx[1]). Hardware wallets and Eternl reject a tx whose structure maps
195
+ * are out of canonical order. Plutus data and multi-asset maps are deliberately NOT checked — they
196
+ * are hashed as-is and canonicalizing them would change hashes.
197
+ */
198
+ const findCanonicalCborViolations = (txHex) => {
199
+ const b = Buffer.from(txHex, 'hex');
200
+ const violations = [];
201
+ const walk = (p, path, checkMapKeys) => {
202
+ const { majorType, additionalInfo, length, headerEnd } = (0, exports.readCborHeader)(b, p);
203
+ const indefinite = additionalInfo === 31;
204
+ let q = headerEnd;
205
+ switch (majorType) {
206
+ case 0:
207
+ case 1:
208
+ case 7:
209
+ return q;
210
+ case 2:
211
+ case 3:
212
+ if (indefinite) {
213
+ while (b[q] !== 0xff)
214
+ q = walk(q, path, false);
215
+ return q + 1;
216
+ }
217
+ return q + length;
218
+ case 4: {
219
+ let i = 0;
220
+ while (indefinite ? b[q] !== 0xff : i < length)
221
+ q = walk(q, `${path}[${i++}]`, false);
222
+ return indefinite ? q + 1 : q;
223
+ }
224
+ case 5: {
225
+ if (indefinite && checkMapKeys)
226
+ violations.push({ path, detail: 'indefinite-length transaction map' });
227
+ let prevKey = null;
228
+ for (let i = 0; indefinite ? b[q] !== 0xff : i < length; i++) {
229
+ const keyStart = q;
230
+ q = walk(q, `${path}{k${i}}`, false);
231
+ if (checkMapKeys) {
232
+ const keyBytes = Buffer.from(b.subarray(keyStart, q));
233
+ if (prevKey && compareEncodedKeys(prevKey, keyBytes) > 0) {
234
+ violations.push({
235
+ path,
236
+ detail: `key #${i} (0x${keyBytes.toString('hex')}) out of canonical order (follows 0x${prevKey.toString('hex')})`
237
+ });
238
+ }
239
+ prevKey = keyBytes;
240
+ }
241
+ q = walk(q, `${path}{v${i}}`, false);
242
+ }
243
+ return indefinite ? q + 1 : q;
244
+ }
245
+ case 6:
246
+ return walk(q, `${path}/tag(${length})`, false);
247
+ default:
248
+ throw new Error(`bad major type ${majorType} at byte ${p}`);
249
+ }
250
+ };
251
+ try {
252
+ const outer = (0, exports.readCborHeader)(b, 0);
253
+ if (outer.majorType !== 4)
254
+ throw new Error('transaction is not a CBOR array');
255
+ let q = outer.headerEnd;
256
+ for (let i = 0; i < outer.length && q < b.length; i++)
257
+ q = walk(q, `tx[${i}]`, i === 0 || i === 1);
258
+ }
259
+ catch (e) {
260
+ violations.push({ path: 'tx', detail: `parse error: ${e.message}` });
261
+ }
262
+ return violations;
263
+ };
264
+ exports.findCanonicalCborViolations = findCanonicalCborViolations;
265
+ const assertCanonicalCbor = (txHex, label = 'tx') => {
266
+ const violations = (0, exports.findCanonicalCborViolations)(txHex);
267
+ if (violations.length > 0) {
268
+ const lines = violations
269
+ .slice(0, 12)
270
+ .map((v) => ` - ${v.path}: ${v.detail}`)
271
+ .join('\n');
272
+ throw new Error(`CBOR is not canonical (${label}) — a hardware wallet / Eternl would reject this tx at signing:\n${lines}`);
273
+ }
274
+ };
275
+ exports.assertCanonicalCbor = assertCanonicalCbor;
package/tx/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './cbor';
2
+ /** Transaction id = blake2b-256 over the exact body bytes. Use it to prove the tx that was
3
+ * signed/submitted is byte-for-byte the tx the backend built. */
4
+ export declare const txHashFromCbor: (txHex: string) => string;
package/tx/index.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.txHashFromCbor = void 0;
18
+ // Subpath module: `@koralabs/kora-labs-common/tx`. Isomorphic (browser + Node), no network.
19
+ const blakejs_1 = require("blakejs");
20
+ const cbor_1 = require("./cbor");
21
+ __exportStar(require("./cbor"), exports);
22
+ /** Transaction id = blake2b-256 over the exact body bytes. Use it to prove the tx that was
23
+ * signed/submitted is byte-for-byte the tx the backend built. */
24
+ const txHashFromCbor = (txHex) => {
25
+ const bytes = Buffer.from(txHex, 'hex');
26
+ const { start, end } = (0, cbor_1.locateTxBody)(bytes);
27
+ return (0, blakejs_1.blake2bHex)(bytes.subarray(start, end), undefined, 32);
28
+ };
29
+ exports.txHashFromCbor = txHashFromCbor;