@koralabs/kora-labs-common 6.17.0 → 6.18.1
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/onChain.d.ts +6 -1
- package/testing/onChain.js +14 -8
- 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
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type Cip68Standard = 'ft' | 'nft' | 'rft';
|
|
2
|
+
/** CIP-67 labels (with checksum) of the CIP-68 user tokens Blockfrost resolves, and of the reference NFT. */
|
|
3
|
+
export declare const CIP68_REFERENCE_LABEL = "000643b0";
|
|
4
|
+
/** For a CIP-68 user token (222/333/444) asset-name hex: its standard and reference-NFT asset-name hex. */
|
|
5
|
+
export declare const cip68ReferenceOf: (hex: string) => {
|
|
6
|
+
standard: Cip68Standard;
|
|
7
|
+
referenceHex: string;
|
|
8
|
+
} | null;
|
|
9
|
+
/** Port of the UTF-8 validator Blockfrost uses (websockets/utf-8-validate fallback). */
|
|
10
|
+
export declare const isValidUTF8: (buf: Buffer) => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Blockfrost's `onchain_metadata` for a CIP-68 token whose reference NFT carries `datumHex`, or null
|
|
13
|
+
* when the datum is not a CIP-68 `Constr [metadata_map, version, extra?]`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const cip68OnchainMetadata: (datumHex: string, standard: Cip68Standard) => Record<string, unknown> | null;
|
|
@@ -0,0 +1,217 @@
|
|
|
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.cip68OnchainMetadata = exports.isValidUTF8 = exports.cip68ReferenceOf = exports.CIP68_REFERENCE_LABEL = void 0;
|
|
7
|
+
// CIP-68 reference-datum -> `onchain_metadata`, exactly as Blockfrost reports it for `/assets/{unit}`.
|
|
8
|
+
//
|
|
9
|
+
// Ported from blockfrost-utils `src/cip68.ts` (getMetadataFromOutputDatum / convertDatumValue,
|
|
10
|
+
// github.com/blockfrost/blockfrost-utils @ b467b2a). Providers without that endpoint (Koios) read the
|
|
11
|
+
// reference token's datum and convert it here, so every consumer sees ONE metadata shape whichever
|
|
12
|
+
// provider answered. Blockfrost's quirks are kept on purpose: only the CIP-68 schema fields of the
|
|
13
|
+
// token's standard are decoded (bytes -> UTF-8, or hex when not valid UTF-8); every other field keeps
|
|
14
|
+
// the value's ORIGINAL CBOR hex (consumers such as the HAL BFF decode those themselves).
|
|
15
|
+
const cbor_1 = __importDefault(require("cbor"));
|
|
16
|
+
const cbor_2 = require("../../tx/cbor");
|
|
17
|
+
const FILES = {
|
|
18
|
+
type: 'array',
|
|
19
|
+
optional: true,
|
|
20
|
+
items: {
|
|
21
|
+
name: { type: 'bytestring', optional: true },
|
|
22
|
+
mediaType: { type: 'bytestring' },
|
|
23
|
+
src: { type: 'bytestring' }
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const METADATA_SCHEME_MAP = {
|
|
27
|
+
ft: {
|
|
28
|
+
name: { type: 'bytestring' },
|
|
29
|
+
description: { type: 'bytestring' },
|
|
30
|
+
ticker: { type: 'bytestring', optional: true },
|
|
31
|
+
url: { type: 'bytestring', optional: true },
|
|
32
|
+
logo: { type: 'bytestring', optional: true },
|
|
33
|
+
decimals: { type: 'number', optional: true }
|
|
34
|
+
},
|
|
35
|
+
nft: {
|
|
36
|
+
name: { type: 'bytestring' },
|
|
37
|
+
image: { type: 'bytestring' },
|
|
38
|
+
mediaType: { type: 'bytestring', optional: true },
|
|
39
|
+
description: { type: 'bytestring', optional: true },
|
|
40
|
+
files: FILES
|
|
41
|
+
},
|
|
42
|
+
rft: {
|
|
43
|
+
name: { type: 'bytestring' },
|
|
44
|
+
image: { type: 'bytestring' },
|
|
45
|
+
mediaType: { type: 'bytestring', optional: true },
|
|
46
|
+
description: { type: 'bytestring', optional: true },
|
|
47
|
+
decimals: { type: 'number', optional: true },
|
|
48
|
+
files: FILES
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/** CIP-67 labels (with checksum) of the CIP-68 user tokens Blockfrost resolves, and of the reference NFT. */
|
|
52
|
+
exports.CIP68_REFERENCE_LABEL = '000643b0';
|
|
53
|
+
const USER_TOKEN_STANDARDS = { '000de140': 'nft', '0014df10': 'ft', '001bc280': 'rft' };
|
|
54
|
+
/** For a CIP-68 user token (222/333/444) asset-name hex: its standard and reference-NFT asset-name hex. */
|
|
55
|
+
const cip68ReferenceOf = (hex) => {
|
|
56
|
+
const standard = USER_TOKEN_STANDARDS[hex.slice(0, 8)];
|
|
57
|
+
return standard ? { standard, referenceHex: `${exports.CIP68_REFERENCE_LABEL}${hex.slice(8)}` } : null;
|
|
58
|
+
};
|
|
59
|
+
exports.cip68ReferenceOf = cip68ReferenceOf;
|
|
60
|
+
/** Port of the UTF-8 validator Blockfrost uses (websockets/utf-8-validate fallback). */
|
|
61
|
+
const isValidUTF8 = (buf) => {
|
|
62
|
+
const len = buf.length;
|
|
63
|
+
let i = 0;
|
|
64
|
+
while (i < len) {
|
|
65
|
+
if ((buf[i] & 0x80) === 0x00) {
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
else if ((buf[i] & 0xe0) === 0xc0) {
|
|
69
|
+
if (i + 1 === len || (buf[i + 1] & 0xc0) !== 0x80 || (buf[i] & 0xfe) === 0xc0)
|
|
70
|
+
return false;
|
|
71
|
+
i += 2;
|
|
72
|
+
}
|
|
73
|
+
else if ((buf[i] & 0xf0) === 0xe0) {
|
|
74
|
+
if (i + 2 >= len ||
|
|
75
|
+
(buf[i + 1] & 0xc0) !== 0x80 ||
|
|
76
|
+
(buf[i + 2] & 0xc0) !== 0x80 ||
|
|
77
|
+
(buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) ||
|
|
78
|
+
(buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
i += 3;
|
|
82
|
+
}
|
|
83
|
+
else if ((buf[i] & 0xf8) === 0xf0) {
|
|
84
|
+
if (i + 3 >= len ||
|
|
85
|
+
(buf[i + 1] & 0xc0) !== 0x80 ||
|
|
86
|
+
(buf[i + 2] & 0xc0) !== 0x80 ||
|
|
87
|
+
(buf[i + 3] & 0xc0) !== 0x80 ||
|
|
88
|
+
(buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) ||
|
|
89
|
+
(buf[i] === 0xf4 && buf[i + 1] > 0x8f) ||
|
|
90
|
+
buf[i] > 0xf4) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
i += 4;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
};
|
|
101
|
+
exports.isValidUTF8 = isValidUTF8;
|
|
102
|
+
const toUTF8OrHex = (buffer) => ((0, exports.isValidUTF8)(buffer) ? buffer.toString('utf8') : buffer.toString('hex'));
|
|
103
|
+
const convertDatumValue = (decodedValue, schema) => {
|
|
104
|
+
var _a;
|
|
105
|
+
if (!schema)
|
|
106
|
+
return null;
|
|
107
|
+
const scheme = schema;
|
|
108
|
+
if (scheme.type === 'number' && typeof decodedValue === 'number')
|
|
109
|
+
return decodedValue;
|
|
110
|
+
if (scheme.type === 'bytestring' && Buffer.isBuffer(decodedValue))
|
|
111
|
+
return toUTF8OrHex(decodedValue);
|
|
112
|
+
if (scheme.type === 'bytestring' && Array.isArray(decodedValue))
|
|
113
|
+
return toUTF8OrHex(Buffer.concat(decodedValue));
|
|
114
|
+
if (Array.isArray(decodedValue)) {
|
|
115
|
+
const converted = [];
|
|
116
|
+
for (const item of decodedValue) {
|
|
117
|
+
const value = convertDatumValue(item, (_a = scheme.items) !== null && _a !== void 0 ? _a : null);
|
|
118
|
+
if (value === null)
|
|
119
|
+
return null;
|
|
120
|
+
converted.push(value);
|
|
121
|
+
}
|
|
122
|
+
return converted;
|
|
123
|
+
}
|
|
124
|
+
if (decodedValue instanceof Map) {
|
|
125
|
+
const metadataMap = {};
|
|
126
|
+
for (const [key, mapValue] of decodedValue.entries()) {
|
|
127
|
+
const convertedKey = Buffer.isBuffer(key) ? toUTF8OrHex(key) : key;
|
|
128
|
+
const valueSchema = schema && convertedKey in schema ? schema[convertedKey] : null;
|
|
129
|
+
const value = convertDatumValue(mapValue, valueSchema);
|
|
130
|
+
if (value === null)
|
|
131
|
+
return null;
|
|
132
|
+
metadataMap[convertedKey] = value;
|
|
133
|
+
}
|
|
134
|
+
return metadataMap;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
/** Start/end of the constructor's fields list: `#6.121..127/1280..1400([fields])` or `#6.102([index, [fields]])`. */
|
|
139
|
+
const constrFields = (bytes) => {
|
|
140
|
+
const tag = (0, cbor_2.readCborHeader)(bytes, 0);
|
|
141
|
+
if (tag.majorType !== 6)
|
|
142
|
+
return null;
|
|
143
|
+
const isCompact = (tag.length >= 121 && tag.length <= 127) || (tag.length >= 1280 && tag.length <= 1400);
|
|
144
|
+
if (isCompact)
|
|
145
|
+
return tag.headerEnd;
|
|
146
|
+
if (tag.length !== 102)
|
|
147
|
+
return null;
|
|
148
|
+
const pair = (0, cbor_2.readCborHeader)(bytes, tag.headerEnd);
|
|
149
|
+
return pair.majorType === 4 ? (0, cbor_2.skipCborItem)(bytes, pair.headerEnd) : null;
|
|
150
|
+
};
|
|
151
|
+
/** The CBOR items of the list starting at `start` (definite or indefinite), as [start, end) spans. */
|
|
152
|
+
const listItems = (bytes, start) => {
|
|
153
|
+
const header = (0, cbor_2.readCborHeader)(bytes, start);
|
|
154
|
+
if (header.majorType !== 4)
|
|
155
|
+
return null;
|
|
156
|
+
const spans = [];
|
|
157
|
+
let cursor = header.headerEnd;
|
|
158
|
+
const indefinite = header.additionalInfo === 31;
|
|
159
|
+
for (let i = 0; indefinite ? bytes[cursor] !== 0xff : i < header.length; i++) {
|
|
160
|
+
const end = (0, cbor_2.skipCborItem)(bytes, cursor);
|
|
161
|
+
spans.push([cursor, end]);
|
|
162
|
+
cursor = end;
|
|
163
|
+
}
|
|
164
|
+
return spans;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Blockfrost's `onchain_metadata` for a CIP-68 token whose reference NFT carries `datumHex`, or null
|
|
168
|
+
* when the datum is not a CIP-68 `Constr [metadata_map, version, extra?]`.
|
|
169
|
+
*/
|
|
170
|
+
const cip68OnchainMetadata = (datumHex, standard) => {
|
|
171
|
+
const bytes = Buffer.from(datumHex, 'hex');
|
|
172
|
+
let fields;
|
|
173
|
+
try {
|
|
174
|
+
const fieldsStart = constrFields(bytes);
|
|
175
|
+
fields = fieldsStart === null ? null : listItems(bytes, fieldsStart);
|
|
176
|
+
}
|
|
177
|
+
catch (_a) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
// [metadata, version] are mandatory
|
|
181
|
+
if (!fields || fields.length < 2)
|
|
182
|
+
return null;
|
|
183
|
+
const [mapStart] = fields[0];
|
|
184
|
+
const map = (0, cbor_2.readCborHeader)(bytes, mapStart);
|
|
185
|
+
if (map.majorType !== 5)
|
|
186
|
+
return null;
|
|
187
|
+
const schema = METADATA_SCHEME_MAP[standard];
|
|
188
|
+
const metadata = {};
|
|
189
|
+
const seenKeys = new Set();
|
|
190
|
+
let cursor = map.headerEnd;
|
|
191
|
+
const indefinite = map.additionalInfo === 31;
|
|
192
|
+
for (let i = 0; indefinite ? bytes[cursor] !== 0xff : i < map.length; i++) {
|
|
193
|
+
const keyEnd = (0, cbor_2.skipCborItem)(bytes, cursor);
|
|
194
|
+
const valueEnd = (0, cbor_2.skipCborItem)(bytes, keyEnd);
|
|
195
|
+
const keyBytes = bytes.subarray(cursor, keyEnd);
|
|
196
|
+
const valueBytes = bytes.subarray(keyEnd, valueEnd);
|
|
197
|
+
cursor = valueEnd;
|
|
198
|
+
// CSL's PlutusMap groups a repeated key's values and Blockfrost reads the first one.
|
|
199
|
+
const keyId = keyBytes.toString('hex');
|
|
200
|
+
if (seenKeys.has(keyId))
|
|
201
|
+
continue;
|
|
202
|
+
seenKeys.add(keyId);
|
|
203
|
+
const decodedKey = cbor_1.default.decodeFirstSync(keyBytes);
|
|
204
|
+
const convertedKey = Buffer.isBuffer(decodedKey) ? toUTF8OrHex(decodedKey) : decodedKey;
|
|
205
|
+
const valueHex = valueBytes.toString('hex');
|
|
206
|
+
if (!(schema && convertedKey in schema)) {
|
|
207
|
+
// Custom field not covered by the CIP-68 standard: unparsed CBOR
|
|
208
|
+
metadata[convertedKey] = valueHex;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const converted = convertDatumValue(cbor_1.default.decodeFirstSync(valueBytes), schema[convertedKey]);
|
|
212
|
+
metadata[convertedKey] = converted !== null ? converted : valueHex;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return metadata;
|
|
216
|
+
};
|
|
217
|
+
exports.cip68OnchainMetadata = cip68OnchainMetadata;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccountAsset, AddressInfo, BackgroundImageDetails, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from './interfaces';
|
|
1
|
+
import { AccountAsset, AddressInfo, BackgroundImageDetails, ChainProtocolParameters, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from './interfaces';
|
|
2
2
|
export interface FailoverOptions {
|
|
3
3
|
/**
|
|
4
4
|
* Cooldown (ms) during which a provider that just failed is skipped as the *start* provider,
|
|
@@ -42,9 +42,14 @@ export declare class ChainProviderFailover {
|
|
|
42
42
|
getRefScriptCbor(tx: string, policyId: string, hex: string): Promise<string>;
|
|
43
43
|
getTxUtxos(tx: string): Promise<ChainProviderUtxo>;
|
|
44
44
|
getAssetUtxo(policyId: string, hex: string): Promise<ChainProviderAsset | null>;
|
|
45
|
+
/** Index of the Blockfrost provider, if configured (start provider for freshness-critical reads). */
|
|
46
|
+
private blockfrostIndex;
|
|
45
47
|
getAssetsByStakeKey(stakeKey: string): Promise<AccountAsset[]>;
|
|
46
48
|
getDatumFromHash(datumHash: string): Promise<string | null>;
|
|
47
49
|
getBackgroundImageDetails(policyId: string, hex: string): Promise<BackgroundImageDetails>;
|
|
48
50
|
getAddressInfo(bech32Address: string): Promise<AddressInfo>;
|
|
49
51
|
getAddressUTxOs(bech32Address: string): Promise<ChainProviderAsset[]>;
|
|
52
|
+
getProtocolParameters(): Promise<ChainProtocolParameters>;
|
|
53
|
+
getTxOutputConsumer(txHash: string, outputIndex: number): Promise<string | null>;
|
|
54
|
+
getAssetOnchainMetadata(policyId: string, hex: string): Promise<Record<string, unknown> | null>;
|
|
50
55
|
}
|
|
@@ -29,18 +29,22 @@ class ChainProviderFailover {
|
|
|
29
29
|
getNextProviderIndex(currentIndex) {
|
|
30
30
|
return (currentIndex + 1) % this.providers.length;
|
|
31
31
|
}
|
|
32
|
-
async getResponse(funcName, args, currentIndex, runNum) {
|
|
32
|
+
async getResponse(funcName, args, currentIndex, runNum, notFound) {
|
|
33
33
|
const num = runNum !== null && runNum !== void 0 ? runNum : 0;
|
|
34
34
|
const index = currentIndex !== null && currentIndex !== void 0 ? currentIndex : this.getStartIndex();
|
|
35
35
|
const chainProvider = this.providers[index];
|
|
36
36
|
return await chainProvider[funcName](...args).catch((error) => {
|
|
37
|
+
// A 404 is an answer ("not on chain (yet)"), not a provider failure: it still falls
|
|
38
|
+
// through, but without benching the provider, and it outranks another provider's outage.
|
|
39
|
+
const isNotFound = (error === null || error === void 0 ? void 0 : error.status) === 404;
|
|
37
40
|
// Mark the failed provider unhealthy for the cooldown window so it isn't chosen as the
|
|
38
41
|
// start provider again immediately; it remains a valid fallback.
|
|
39
|
-
if (this.cooldownMs > 0)
|
|
42
|
+
if (this.cooldownMs > 0 && !isNotFound)
|
|
40
43
|
this.unhealthyUntil[index] = this.now() + this.cooldownMs;
|
|
44
|
+
const answer = notFound !== null && notFound !== void 0 ? notFound : (isNotFound ? error : undefined);
|
|
41
45
|
if (num === this.providers.length - 1)
|
|
42
|
-
throw error;
|
|
43
|
-
return this.getResponse(funcName, args, this.getNextProviderIndex(index), num + 1);
|
|
46
|
+
throw answer !== null && answer !== void 0 ? answer : error;
|
|
47
|
+
return this.getResponse(funcName, args, this.getNextProviderIndex(index), num + 1, answer);
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
async getLatestTransactionForAsset(policyId, hex) {
|
|
@@ -69,8 +73,12 @@ class ChainProviderFailover {
|
|
|
69
73
|
// provider lagging a burn returns a stale UTxO and falsely resurrects a de-indexed handle.
|
|
70
74
|
// Prefer Blockfrost (reads the latest asset tx live; Koios's tx index can lag a burn by
|
|
71
75
|
// minutes). Koios stays the failover on error.
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
return this.getResponse('getAssetUtxo', [policyId, hex], this.blockfrostIndex());
|
|
77
|
+
}
|
|
78
|
+
/** Index of the Blockfrost provider, if configured (start provider for freshness-critical reads). */
|
|
79
|
+
blockfrostIndex() {
|
|
80
|
+
const index = this.providers.findIndex((p) => { var _a; return ((_a = p.name) !== null && _a !== void 0 ? _a : '').toLowerCase().includes('blockfrost'); });
|
|
81
|
+
return index >= 0 ? index : undefined;
|
|
74
82
|
}
|
|
75
83
|
async getAssetsByStakeKey(stakeKey) {
|
|
76
84
|
return this.getResponse('getAssetsByStakeKey', [stakeKey]);
|
|
@@ -87,5 +95,15 @@ class ChainProviderFailover {
|
|
|
87
95
|
async getAddressUTxOs(bech32Address) {
|
|
88
96
|
return this.getResponse('getAddressUTxOs', [bech32Address]);
|
|
89
97
|
}
|
|
98
|
+
async getProtocolParameters() {
|
|
99
|
+
return this.getResponse('getProtocolParameters', []);
|
|
100
|
+
}
|
|
101
|
+
async getTxOutputConsumer(txHash, outputIndex) {
|
|
102
|
+
// Blockfrost answers from its spent-by index in one call; Koios has to scan the address's txs.
|
|
103
|
+
return this.getResponse('getTxOutputConsumer', [txHash, outputIndex], this.blockfrostIndex());
|
|
104
|
+
}
|
|
105
|
+
async getAssetOnchainMetadata(policyId, hex) {
|
|
106
|
+
return this.getResponse('getAssetOnchainMetadata', [policyId, hex]);
|
|
107
|
+
}
|
|
90
108
|
}
|
|
91
109
|
exports.ChainProviderFailover = ChainProviderFailover;
|
|
@@ -28,6 +28,26 @@ export interface AddressInfo {
|
|
|
28
28
|
stake_address?: string | null;
|
|
29
29
|
script: boolean;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Current protocol parameters in Blockfrost's `/epochs/latest/parameters` field names (Koios's
|
|
33
|
+
* `/epoch_params` uses the same names and reports identical values; its `cost_models` is mapped to
|
|
34
|
+
* `cost_models_raw`). Cost models are ledger-ordered arrays keyed `PlutusV1`/`PlutusV2`/`PlutusV3`.
|
|
35
|
+
* `@koralabs/kora-labs-common/txBuild` `protocolParametersFromChain` turns this into tx-builder params.
|
|
36
|
+
*/
|
|
37
|
+
export interface ChainProtocolParameters {
|
|
38
|
+
epoch: number;
|
|
39
|
+
min_fee_a: number;
|
|
40
|
+
min_fee_b: number;
|
|
41
|
+
max_tx_size: number;
|
|
42
|
+
coins_per_utxo_size: string;
|
|
43
|
+
key_deposit: string;
|
|
44
|
+
price_mem: number;
|
|
45
|
+
price_step: number;
|
|
46
|
+
max_tx_ex_mem: string;
|
|
47
|
+
max_tx_ex_steps: string;
|
|
48
|
+
min_fee_ref_script_cost_per_byte: number | null;
|
|
49
|
+
cost_models_raw: Record<string, number[]>;
|
|
50
|
+
}
|
|
31
51
|
export interface BackgroundImageDetails {
|
|
32
52
|
image: string;
|
|
33
53
|
creatorDefaults?: ICreatorDefaults;
|
|
@@ -52,11 +72,24 @@ export interface ChainProvider {
|
|
|
52
72
|
getAssetDatum(policyId: string, hex: string): Promise<string | null>;
|
|
53
73
|
getCip25AssetImage(policyId: string, hex: string): Promise<string | null>;
|
|
54
74
|
getRefScriptCbor(tx: string, policyId: string, hex: string): Promise<string>;
|
|
75
|
+
/** The tx's outputs. Rejects with `status: 404` when the tx is not on chain (not in a block yet). */
|
|
55
76
|
getTxUtxos(tx: string): Promise<ChainProviderUtxo>;
|
|
56
77
|
getAssetUtxo(policyId: string, hex: string): Promise<ChainProviderAsset | null>;
|
|
57
78
|
getAssetsByStakeKey(stakeKey: string): Promise<AccountAsset[]>;
|
|
58
79
|
getDatumFromHash(datumHash: string): Promise<string | null>;
|
|
59
80
|
getBackgroundImageDetails(policyId: string, hex: string): Promise<BackgroundImageDetails>;
|
|
60
81
|
getAddressInfo(bech32Address: string): Promise<AddressInfo>;
|
|
82
|
+
/** Every unspent output at the address (all pages). An address with no UTxOs has none. */
|
|
61
83
|
getAddressUTxOs(bech32Address: string): Promise<ChainProviderAsset[]>;
|
|
84
|
+
getProtocolParameters(): Promise<ChainProtocolParameters>;
|
|
85
|
+
/**
|
|
86
|
+
* The tx that spent output `txHash#outputIndex`, or null while it is unspent. Rejects with
|
|
87
|
+
* `status: 404` when that output does not exist (e.g. the tx is not in a block yet).
|
|
88
|
+
*/
|
|
89
|
+
getTxOutputConsumer(txHash: string, outputIndex: number): Promise<string | null>;
|
|
90
|
+
/**
|
|
91
|
+
* The asset's `onchain_metadata` as Blockfrost reports it (CIP-68 reference datum converted per
|
|
92
|
+
* `cip68OnchainMetadata`, else the CIP-25 721 entry), or null when it has none.
|
|
93
|
+
*/
|
|
94
|
+
getAssetOnchainMetadata(policyId: string, hex: string): Promise<Record<string, unknown> | null>;
|
|
62
95
|
}
|
package/chain/index.d.ts
CHANGED
package/chain/index.js
CHANGED
|
@@ -27,3 +27,4 @@ __exportStar(require("./config"), exports);
|
|
|
27
27
|
__exportStar(require("./datum/imageDatum"), exports);
|
|
28
28
|
__exportStar(require("./providers/Koios"), exports);
|
|
29
29
|
__exportStar(require("./providers/Blockfrost"), exports);
|
|
30
|
+
__exportStar(require("./datum/cip68Metadata"), exports);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ICreatorDefaults } from '../../handles/interfaces';
|
|
2
|
-
import { AccountAsset, AddressInfo, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from '../failover/interfaces';
|
|
2
|
+
import { AccountAsset, AddressInfo, ChainProtocolParameters, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from '../failover/interfaces';
|
|
3
3
|
import { ChainProviderConfig } from '../providerConfig';
|
|
4
4
|
export interface DrepMetadataValue {
|
|
5
5
|
'@value': string;
|
|
@@ -55,5 +55,8 @@ export declare class Blockfrost implements ChainProvider {
|
|
|
55
55
|
}>;
|
|
56
56
|
getAddressInfo(bech32Address: string): Promise<AddressInfo>;
|
|
57
57
|
getAddressUTxOs(bech32Address: string): Promise<ChainProviderAsset[]>;
|
|
58
|
+
getProtocolParameters(): Promise<ChainProtocolParameters>;
|
|
59
|
+
getTxOutputConsumer(txHash: string, outputIndex: number): Promise<string | null>;
|
|
60
|
+
getAssetOnchainMetadata(policyId: string, hex: string): Promise<Record<string, unknown> | null>;
|
|
58
61
|
}
|
|
59
62
|
export {};
|
|
@@ -138,7 +138,60 @@ class Blockfrost {
|
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
async getAddressUTxOs(bech32Address) {
|
|
141
|
-
|
|
141
|
+
// Blockfrost pages at 100: never truncate. An address that never received funds is a 404.
|
|
142
|
+
const all = [];
|
|
143
|
+
for (let page = 1;; page++) {
|
|
144
|
+
let batch;
|
|
145
|
+
try {
|
|
146
|
+
batch = await this.fetchBlockfrost(`/addresses/${bech32Address}/utxos?count=100&page=${page}`);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if ((error === null || error === void 0 ? void 0 : error.status) === 404)
|
|
150
|
+
return all;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
all.push(...batch);
|
|
154
|
+
if (batch.length < 100)
|
|
155
|
+
return all;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async getProtocolParameters() {
|
|
159
|
+
var _a;
|
|
160
|
+
const p = await this.fetchBlockfrost('epochs/latest/parameters');
|
|
161
|
+
return {
|
|
162
|
+
epoch: p.epoch,
|
|
163
|
+
min_fee_a: p.min_fee_a,
|
|
164
|
+
min_fee_b: p.min_fee_b,
|
|
165
|
+
max_tx_size: p.max_tx_size,
|
|
166
|
+
coins_per_utxo_size: String(p.coins_per_utxo_size),
|
|
167
|
+
key_deposit: String(p.key_deposit),
|
|
168
|
+
price_mem: p.price_mem,
|
|
169
|
+
price_step: p.price_step,
|
|
170
|
+
max_tx_ex_mem: String(p.max_tx_ex_mem),
|
|
171
|
+
max_tx_ex_steps: String(p.max_tx_ex_steps),
|
|
172
|
+
min_fee_ref_script_cost_per_byte: (_a = p.min_fee_ref_script_cost_per_byte) !== null && _a !== void 0 ? _a : null,
|
|
173
|
+
cost_models_raw: p.cost_models_raw
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async getTxOutputConsumer(txHash, outputIndex) {
|
|
177
|
+
var _a;
|
|
178
|
+
const { outputs } = await this.getTxUtxos(txHash);
|
|
179
|
+
const output = outputs.find((o) => o.output_index === outputIndex);
|
|
180
|
+
if (!output)
|
|
181
|
+
throw (0, fetchProviderJson_1.providerNotFoundError)('Blockfrost', `Output ${txHash}#${outputIndex}`);
|
|
182
|
+
return (_a = output.consumed_by_tx) !== null && _a !== void 0 ? _a : null;
|
|
183
|
+
}
|
|
184
|
+
async getAssetOnchainMetadata(policyId, hex) {
|
|
185
|
+
var _a;
|
|
186
|
+
try {
|
|
187
|
+
const asset = await this.fetchBlockfrost(`/assets/${policyId}${hex}`);
|
|
188
|
+
return (_a = asset === null || asset === void 0 ? void 0 : asset.onchain_metadata) !== null && _a !== void 0 ? _a : null;
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if ((error === null || error === void 0 ? void 0 : error.status) === 404)
|
|
192
|
+
return null;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
142
195
|
}
|
|
143
196
|
}
|
|
144
197
|
exports.Blockfrost = Blockfrost;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ICreatorDefaults } from '../../handles/interfaces';
|
|
2
|
-
import { AccountAsset, AddressInfo, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from '../failover/interfaces';
|
|
2
|
+
import { AccountAsset, AddressInfo, ChainProtocolParameters, ChainProvider, ChainProviderAsset, ChainProviderUtxo } from '../failover/interfaces';
|
|
3
3
|
import { ChainProviderConfig } from '../providerConfig';
|
|
4
4
|
interface KoiosAsset {
|
|
5
5
|
quantity: string;
|
|
@@ -17,9 +17,11 @@ interface KoiosTxOutput {
|
|
|
17
17
|
bytes: string | null;
|
|
18
18
|
value: string | null;
|
|
19
19
|
} | null;
|
|
20
|
-
payment_addr
|
|
20
|
+
/** tx_info outputs carry `payment_addr`; address_utxos / utxo_info rows carry `address` instead. */
|
|
21
|
+
payment_addr?: {
|
|
21
22
|
bech32: string;
|
|
22
23
|
};
|
|
24
|
+
address?: string;
|
|
23
25
|
reference_script: {
|
|
24
26
|
bytes: string | null;
|
|
25
27
|
hash: string | null;
|
|
@@ -64,5 +66,8 @@ export declare class Koios implements ChainProvider {
|
|
|
64
66
|
}>;
|
|
65
67
|
getAddressInfo(bech32Address: string): Promise<AddressInfo>;
|
|
66
68
|
getAddressUTxOs(bech32Address: string): Promise<ChainProviderAsset[]>;
|
|
69
|
+
getProtocolParameters(): Promise<ChainProtocolParameters>;
|
|
70
|
+
getTxOutputConsumer(txHash: string, outputIndex: number): Promise<string | null>;
|
|
71
|
+
getAssetOnchainMetadata(policyId: string, hex: string): Promise<Record<string, unknown> | null>;
|
|
67
72
|
}
|
|
68
73
|
export {};
|
package/chain/providers/Koios.js
CHANGED
|
@@ -4,7 +4,12 @@ exports.Koios = exports.buildKoiosAddressUtxoRequestBody = exports.buildKoiosHea
|
|
|
4
4
|
const types_1 = require("../../types");
|
|
5
5
|
const fetchProviderJson_1 = require("../transport/fetchProviderJson");
|
|
6
6
|
const imageDatum_1 = require("../datum/imageDatum");
|
|
7
|
+
const cip68Metadata_1 = require("../datum/cip68Metadata");
|
|
7
8
|
const providerConfig_1 = require("../providerConfig");
|
|
9
|
+
/** PostgREST page size Koios serves (its maximum). */
|
|
10
|
+
const KOIOS_PAGE = 1000;
|
|
11
|
+
/** Tx hashes per tx_info request when scanning for an output's consumer. */
|
|
12
|
+
const TX_INFO_BATCH = 50;
|
|
8
13
|
const buildKoiosHeaders = (bearerToken) => ({
|
|
9
14
|
'Content-Type': 'application/json',
|
|
10
15
|
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {})
|
|
@@ -58,7 +63,7 @@ class Koios {
|
|
|
58
63
|
}, [])) !== null && _a !== void 0 ? _a : []);
|
|
59
64
|
}
|
|
60
65
|
async getAssetUtxo(policyId, hex) {
|
|
61
|
-
var _a, _b, _c;
|
|
66
|
+
var _a, _b, _c, _d;
|
|
62
67
|
const tx = await this.getLatestTransactionForAsset(policyId, hex);
|
|
63
68
|
if (!tx)
|
|
64
69
|
return null;
|
|
@@ -74,9 +79,9 @@ class Koios {
|
|
|
74
79
|
}));
|
|
75
80
|
amount.push({ unit: 'lovelace', quantity: output.value });
|
|
76
81
|
const chainProviderAsset = {
|
|
77
|
-
address: output.payment_addr.bech32,
|
|
82
|
+
address: (_b = output.address) !== null && _b !== void 0 ? _b : output.payment_addr.bech32,
|
|
78
83
|
amount,
|
|
79
|
-
inline_datum: (
|
|
84
|
+
inline_datum: (_d = (_c = output.inline_datum) === null || _c === void 0 ? void 0 : _c.bytes) !== null && _d !== void 0 ? _d : null,
|
|
80
85
|
output_index: output.tx_index,
|
|
81
86
|
tx_hash: firstTransaction.tx_hash,
|
|
82
87
|
data_hash: output.datum_hash
|
|
@@ -105,7 +110,7 @@ class Koios {
|
|
|
105
110
|
return (_a = output === null || output === void 0 ? void 0 : output.reference_script_cbor) !== null && _a !== void 0 ? _a : '';
|
|
106
111
|
}
|
|
107
112
|
async buildChainProviderAsset(output) {
|
|
108
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
113
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
109
114
|
const amount = output.asset_list.map((a) => ({
|
|
110
115
|
unit: `${a.policy_id}${a.asset_name}`,
|
|
111
116
|
quantity: a.quantity
|
|
@@ -115,20 +120,22 @@ class Koios {
|
|
|
115
120
|
? await this.getDatumFromHash(output.datum_hash)
|
|
116
121
|
: (_c = (_b = output.inline_datum) === null || _b === void 0 ? void 0 : _b.bytes) !== null && _c !== void 0 ? _c : null;
|
|
117
122
|
return {
|
|
118
|
-
address: output.payment_addr.bech32,
|
|
123
|
+
address: (_d = output.address) !== null && _d !== void 0 ? _d : output.payment_addr.bech32,
|
|
119
124
|
amount,
|
|
120
125
|
inline_datum,
|
|
121
126
|
output_index: output.tx_index,
|
|
122
127
|
tx_hash: output.tx_hash,
|
|
123
128
|
data_hash: output.datum_hash,
|
|
124
|
-
reference_script_hash: (
|
|
125
|
-
reference_script_cbor: (
|
|
129
|
+
reference_script_hash: (_f = (_e = output.reference_script) === null || _e === void 0 ? void 0 : _e.hash) !== null && _f !== void 0 ? _f : null,
|
|
130
|
+
reference_script_cbor: (_h = (_g = output.reference_script) === null || _g === void 0 ? void 0 : _g.bytes) !== null && _h !== void 0 ? _h : null
|
|
126
131
|
};
|
|
127
132
|
}
|
|
128
133
|
async getTxUtxos(tx) {
|
|
129
134
|
var _a;
|
|
130
135
|
const res = await this.fetchKoios(`/tx_info`, 'POST', JSON.stringify({ _tx_hashes: [tx], _metadata: true, _assets: true }));
|
|
131
136
|
const [firstTransaction] = res;
|
|
137
|
+
if (!firstTransaction)
|
|
138
|
+
throw (0, fetchProviderJson_1.providerNotFoundError)('Koios', `Transaction ${tx}`);
|
|
132
139
|
const outputs = [];
|
|
133
140
|
for (const output of (_a = firstTransaction === null || firstTransaction === void 0 ? void 0 : firstTransaction.outputs) !== null && _a !== void 0 ? _a : []) {
|
|
134
141
|
outputs.push(await this.buildChainProviderAsset(output));
|
|
@@ -205,12 +212,74 @@ class Koios {
|
|
|
205
212
|
};
|
|
206
213
|
}
|
|
207
214
|
async getAddressUTxOs(bech32Address) {
|
|
208
|
-
const result = await this.fetchKoios(`/address_utxos`, 'POST', JSON.stringify((0, exports.buildKoiosAddressUtxoRequestBody)(bech32Address)));
|
|
209
215
|
const utxos = [];
|
|
210
|
-
for (
|
|
211
|
-
|
|
216
|
+
for (let offset = 0;; offset += KOIOS_PAGE) {
|
|
217
|
+
const page = await this.fetchKoios(`/address_utxos?offset=${offset}&limit=${KOIOS_PAGE}`, 'POST', JSON.stringify((0, exports.buildKoiosAddressUtxoRequestBody)(bech32Address)));
|
|
218
|
+
for (const output of page) {
|
|
219
|
+
utxos.push(await this.buildChainProviderAsset(output));
|
|
220
|
+
}
|
|
221
|
+
if (page.length < KOIOS_PAGE)
|
|
222
|
+
return utxos;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async getProtocolParameters() {
|
|
226
|
+
var _a;
|
|
227
|
+
// The tip's epoch, not the newest epoch_params row: rows for the next epoch can appear before it starts.
|
|
228
|
+
const [tip] = await this.fetchKoios('/tip');
|
|
229
|
+
const [p] = await this.fetchKoios(`/epoch_params?_epoch_no=${tip.epoch_no}`);
|
|
230
|
+
if (!p)
|
|
231
|
+
throw (0, fetchProviderJson_1.providerNotFoundError)('Koios', `Protocol parameters for epoch ${tip.epoch_no}`);
|
|
232
|
+
return {
|
|
233
|
+
epoch: p.epoch_no,
|
|
234
|
+
min_fee_a: p.min_fee_a,
|
|
235
|
+
min_fee_b: p.min_fee_b,
|
|
236
|
+
max_tx_size: p.max_tx_size,
|
|
237
|
+
coins_per_utxo_size: String(p.coins_per_utxo_size),
|
|
238
|
+
key_deposit: String(p.key_deposit),
|
|
239
|
+
price_mem: p.price_mem,
|
|
240
|
+
price_step: p.price_step,
|
|
241
|
+
max_tx_ex_mem: String(p.max_tx_ex_mem),
|
|
242
|
+
max_tx_ex_steps: String(p.max_tx_ex_steps),
|
|
243
|
+
min_fee_ref_script_cost_per_byte: (_a = p.min_fee_ref_script_cost_per_byte) !== null && _a !== void 0 ? _a : null,
|
|
244
|
+
cost_models_raw: p.cost_models
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async getTxOutputConsumer(txHash, outputIndex) {
|
|
248
|
+
const ref = `${txHash}#${outputIndex}`;
|
|
249
|
+
const [utxo] = await this.fetchKoios('/utxo_info', 'POST', JSON.stringify({ _utxo_refs: [ref], _extended: false }));
|
|
250
|
+
if (!utxo)
|
|
251
|
+
throw (0, fetchProviderJson_1.providerNotFoundError)('Koios', `Output ${ref}`);
|
|
252
|
+
if (!utxo.is_spent)
|
|
253
|
+
return null;
|
|
254
|
+
// Koios has no spent-by index: scan the txs touching the output's address from its block on
|
|
255
|
+
// (inclusive: a chained spend can land in the same block) for the one that consumes it.
|
|
256
|
+
for (let offset = 0;; offset += KOIOS_PAGE) {
|
|
257
|
+
const txs = await this.fetchKoios(`/address_txs?order=block_height.asc&offset=${offset}&limit=${KOIOS_PAGE}`, 'POST', JSON.stringify({ _addresses: [utxo.address], _after_block_height: utxo.block_height }));
|
|
258
|
+
const candidates = txs.map((t) => t.tx_hash).filter((h) => h !== txHash);
|
|
259
|
+
for (let i = 0; i < candidates.length; i += TX_INFO_BATCH) {
|
|
260
|
+
const infos = await this.fetchKoios('/tx_info', 'POST', JSON.stringify({ _tx_hashes: candidates.slice(i, i + TX_INFO_BATCH), _inputs: true, _metadata: false, _assets: false, _withdrawals: false, _certs: false, _scripts: false, _bytecode: false, _governance: false }));
|
|
261
|
+
const consumer = infos.find((t) => { var _a; return (_a = t.inputs) === null || _a === void 0 ? void 0 : _a.some((input) => input.tx_hash === txHash && input.tx_index === outputIndex); });
|
|
262
|
+
if (consumer)
|
|
263
|
+
return consumer.tx_hash;
|
|
264
|
+
}
|
|
265
|
+
if (txs.length < KOIOS_PAGE)
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
throw new Error(`Koios reports ${ref} spent but no tx at ${utxo.address} consumes it`);
|
|
269
|
+
}
|
|
270
|
+
async getAssetOnchainMetadata(policyId, hex) {
|
|
271
|
+
var _a, _b, _c, _d;
|
|
272
|
+
// Blockfrost's precedence: a CIP-68 user token's reference datum, else the CIP-25 721 entry.
|
|
273
|
+
const cip68 = (0, cip68Metadata_1.cip68ReferenceOf)(hex);
|
|
274
|
+
if (cip68) {
|
|
275
|
+
const datum = await this.getAssetDatum(policyId, cip68.referenceHex);
|
|
276
|
+
const metadata = datum ? (0, cip68Metadata_1.cip68OnchainMetadata)(datum, cip68.standard) : null;
|
|
277
|
+
if (metadata)
|
|
278
|
+
return metadata;
|
|
212
279
|
}
|
|
213
|
-
|
|
280
|
+
const [info] = await this.fetchKoios('/asset_info', 'POST', JSON.stringify({ _asset_list: [[policyId, hex]] }));
|
|
281
|
+
const byPolicy = (_b = (_a = info === null || info === void 0 ? void 0 : info.minting_tx_metadata) === null || _a === void 0 ? void 0 : _a['721']) === null || _b === void 0 ? void 0 : _b[policyId];
|
|
282
|
+
return (_d = (_c = byPolicy === null || byPolicy === void 0 ? void 0 : byPolicy[hex]) !== null && _c !== void 0 ? _c : byPolicy === null || byPolicy === void 0 ? void 0 : byPolicy[Buffer.from(hex, 'hex').toString('utf8')]) !== null && _d !== void 0 ? _d : null;
|
|
214
283
|
}
|
|
215
284
|
}
|
|
216
285
|
exports.Koios = Koios;
|
|
@@ -25,6 +25,8 @@ export interface ProviderRequestOptions {
|
|
|
25
25
|
maxRateLimitWaitMs?: number;
|
|
26
26
|
fetcher?: ProviderFetcher;
|
|
27
27
|
}
|
|
28
|
+
/** A provider's "that does not exist" answer, shaped like a transport 404 (`status: 404`). */
|
|
29
|
+
export declare const providerNotFoundError: (provider: ProviderName, what: string) => Error;
|
|
28
30
|
export declare const isRetriableError: (provider: ProviderName, error: any) => boolean;
|
|
29
31
|
export declare const fetchProviderJson: <T>({ provider, url, method, headers, body, maxRetries, retryBaseDelayMs, maxRps, rateLimitKey, maxRateLimitWaitMs, fetcher }: ProviderRequestOptions) => Promise<T>;
|
|
30
32
|
export {};
|
|
@@ -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
package/testing/onChain.d.ts
CHANGED
|
@@ -33,7 +33,12 @@ export interface TxConfirmation {
|
|
|
33
33
|
block?: string;
|
|
34
34
|
validContract?: boolean;
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Poll until the tx is in a block AND Blockfrost serves its UTxOs. Right after inclusion Blockfrost can
|
|
38
|
+
* answer /txs/<hash> while /txs/<hash>/utxos still 404s (seen on preview: the live suite's reads right
|
|
39
|
+
* after "confirmed" failed), so "confirmed" means every follow-up read of the tx will answer.
|
|
40
|
+
* A tx in a block with `valid_contract: false` is NOT a success.
|
|
41
|
+
*/
|
|
37
42
|
export declare const waitForTxConfirmation: (chain: ChainConfig, txHash: string, timeoutMs?: number, pollMs?: number) => Promise<TxConfirmation>;
|
|
38
43
|
/** Poll a tx output until it is spent; returns the spending tx hash, or null on timeout. */
|
|
39
44
|
export declare const waitForOutputConsumed: (chain: ChainConfig, txHash: string, outputIndex: number, timeoutMs: number, pollMs?: number) => Promise<string | null>;
|
package/testing/onChain.js
CHANGED
|
@@ -28,17 +28,23 @@ const fetchTxUtxos = (chain, txHash) => getJson(chain, `/txs/${txHash}/utxos`);
|
|
|
28
28
|
exports.fetchTxUtxos = fetchTxUtxos;
|
|
29
29
|
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'); };
|
|
30
30
|
exports.lovelaceOf = lovelaceOf;
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Poll until the tx is in a block AND Blockfrost serves its UTxOs. Right after inclusion Blockfrost can
|
|
33
|
+
* answer /txs/<hash> while /txs/<hash>/utxos still 404s (seen on preview: the live suite's reads right
|
|
34
|
+
* after "confirmed" failed), so "confirmed" means every follow-up read of the tx will answer.
|
|
35
|
+
* A tx in a block with `valid_contract: false` is NOT a success.
|
|
36
|
+
*/
|
|
32
37
|
const waitForTxConfirmation = async (chain, txHash, timeoutMs = 180000, pollMs = 5000) => {
|
|
33
38
|
const start = Date.now();
|
|
39
|
+
// A transient provider failure is another poll; a rate limit too long to sit out is returned.
|
|
40
|
+
const poll = (path) => (0, blockfrost_1.blockfrostGet)(chain, path).catch((error) => {
|
|
41
|
+
if (error instanceof rateLimit_1.RateLimitedError)
|
|
42
|
+
throw error;
|
|
43
|
+
return null;
|
|
44
|
+
});
|
|
34
45
|
for (;;) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (error instanceof rateLimit_1.RateLimitedError)
|
|
38
|
-
throw error;
|
|
39
|
-
return null;
|
|
40
|
-
});
|
|
41
|
-
if (data)
|
|
46
|
+
const data = await poll(`/txs/${txHash}`);
|
|
47
|
+
if (data && (await poll(`/txs/${txHash}/utxos`)))
|
|
42
48
|
return { confirmed: true, block: data.block, validContract: data.valid_contract };
|
|
43
49
|
if (Date.now() - start >= timeoutMs)
|
|
44
50
|
return { confirmed: false };
|
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
|
package/txBuild/scriptTx.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.addVkeyWitnesses = exports.selectWalletInputs = exports.finalizeScriptTx = exports.certificateDeposit = exports.computeScriptDataHash = exports.withMinAda = void 0;
|
|
3
|
+
exports.addVkeyWitnesses = exports.finalizeWalletFundedTx = exports.selectWalletInputs = exports.finalizeScriptTx = exports.InsufficientInputsError = exports.certificateDeposit = exports.computeScriptDataHash = exports.withMinAda = void 0;
|
|
4
4
|
// Deterministic Plutus transaction finalizer on @cardano-sdk/core (no input selection, no Helios).
|
|
5
5
|
//
|
|
6
6
|
// The caller fixes every input; this module derives redeemer pointers from the ledger's canonical
|
|
@@ -87,6 +87,15 @@ const sumValues = (values) => {
|
|
|
87
87
|
}
|
|
88
88
|
return { coins, assets };
|
|
89
89
|
};
|
|
90
|
+
/** The inputs cannot pay the outputs + fee (+ a min-UTxO change output); `shortfall` more lovelace would. */
|
|
91
|
+
class InsufficientInputsError extends Error {
|
|
92
|
+
constructor(message, shortfall) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.shortfall = shortfall;
|
|
95
|
+
this.name = 'InsufficientInputsError';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.InsufficientInputsError = InsufficientInputsError;
|
|
90
99
|
const finalizeScriptTx = async (plan, params, evaluate) => {
|
|
91
100
|
var _a, _b, _c;
|
|
92
101
|
const inputs = [...plan.inputs].sort((a, b) => byTxIn(a.utxo[0], b.utxo[0]));
|
|
@@ -112,7 +121,7 @@ const finalizeScriptTx = async (plan, params, evaluate) => {
|
|
|
112
121
|
var _a, _b, _c;
|
|
113
122
|
const coins = inValue.coins + withdrawn - deposited - produced.coins - fee;
|
|
114
123
|
if (coins < BigInt(0))
|
|
115
|
-
throw new
|
|
124
|
+
throw new InsufficientInputsError(`Inputs do not cover outputs + fee: ${-coins} lovelace short`, -coins);
|
|
116
125
|
const assets = new Map();
|
|
117
126
|
const ids = new Set([...inValue.assets.keys(), ...tokenMap.keys(), ...produced.assets.keys()]);
|
|
118
127
|
for (const id of ids) {
|
|
@@ -192,8 +201,9 @@ const finalizeScriptTx = async (plan, params, evaluate) => {
|
|
|
192
201
|
throw new Error('Execution units did not converge');
|
|
193
202
|
}
|
|
194
203
|
const { tx, change, redeemers } = assemble(fee, exUnits, new Map());
|
|
195
|
-
|
|
196
|
-
|
|
204
|
+
const changeMinAda = (0, fees_1.minAdaForOutputSize)(params.coinsPerUtxoByte, outputSize(change));
|
|
205
|
+
if (change.value.coins < changeMinAda) {
|
|
206
|
+
throw new InsufficientInputsError(`Inputs do not cover outputs + fee: change of ${change.value.coins} lovelace is below min-UTxO`, changeMinAda - change.value.coins);
|
|
197
207
|
}
|
|
198
208
|
const signedSize = sizeOf(fee, exUnits);
|
|
199
209
|
if (signedSize > params.maxTxSize)
|
|
@@ -231,6 +241,35 @@ const selectWalletInputs = (utxos, lovelace, exclude = new Set()) => {
|
|
|
231
241
|
return selected;
|
|
232
242
|
};
|
|
233
243
|
exports.selectWalletInputs = selectWalletInputs;
|
|
244
|
+
const coinsOf = (utxos) => utxos.reduce((sum, [, out]) => sum + out.value.coins, BigInt(0));
|
|
245
|
+
/**
|
|
246
|
+
* Coin selection + finalization in one: walks the `selectWalletInputs` order (ADA-only first, largest
|
|
247
|
+
* first) one UTxO at a time and returns the first selection `finalizeScriptTx` can balance: inputs
|
|
248
|
+
* covering outputs + fee with a min-UTxO change output. Pure: no chain reads, so chained (unconfirmed)
|
|
249
|
+
* wallet UTxOs are spendable. Throws `InsufficientInputsError` (shortfall vs. the whole wallet) when
|
|
250
|
+
* no selection works.
|
|
251
|
+
*/
|
|
252
|
+
const finalizeWalletFundedTx = async ({ walletUtxos, signerCount, inputs = [], ...plan }, params, evaluate) => {
|
|
253
|
+
const available = coinsOf(walletUtxos);
|
|
254
|
+
let required = plan.outputs.reduce((sum, o) => sum + o.value.coins, BigInt(0));
|
|
255
|
+
// Each selection is the previous one plus the next UTxO in order (+1 lovelace past its total).
|
|
256
|
+
for (let lovelace = BigInt(1); lovelace <= available;) {
|
|
257
|
+
const walletInputs = (0, exports.selectWalletInputs)(walletUtxos, lovelace);
|
|
258
|
+
const selected = coinsOf(walletInputs);
|
|
259
|
+
try {
|
|
260
|
+
const tx = await (0, exports.finalizeScriptTx)({ ...plan, inputs: [...inputs, ...walletInputs.map((utxo) => ({ utxo }))], signerCount: signerCount(walletInputs) }, params, evaluate);
|
|
261
|
+
return { ...tx, walletInputs };
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
if (!(error instanceof InsufficientInputsError))
|
|
265
|
+
throw error;
|
|
266
|
+
required = selected + error.shortfall;
|
|
267
|
+
lovelace = selected + BigInt(1);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
throw new InsufficientInputsError(`Wallet holds ${available} spendable lovelace; about ${required} needed`, required - available);
|
|
271
|
+
};
|
|
272
|
+
exports.finalizeWalletFundedTx = finalizeWalletFundedTx;
|
|
234
273
|
/**
|
|
235
274
|
* Add vkey witnesses to an unsigned tx by splicing them into its witness set byte-for-byte
|
|
236
275
|
* (`mergeWitnessSet`): the body (so the tx id and every signature) and the redeemer bytes the
|