@odatano/core 0.3.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.
Files changed (98) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +212 -0
  3. package/cds-plugin.js +5 -0
  4. package/config/preview/cardano-node/alonzo-genesis.json +196 -0
  5. package/config/preview/cardano-node/byron-genesis.json +117 -0
  6. package/config/preview/cardano-node/config.json +118 -0
  7. package/config/preview/cardano-node/conway-genesis.json +297 -0
  8. package/config/preview/cardano-node/shelley-genesis.json +68 -0
  9. package/config/preview/cardano-node/topology.json +19 -0
  10. package/db/schema.cds +1318 -0
  11. package/package.json +125 -0
  12. package/src/index.d.ts.map +1 -0
  13. package/src/index.js +96 -0
  14. package/src/index.js.map +1 -0
  15. package/src/plugin.d.ts.map +1 -0
  16. package/src/plugin.js +92 -0
  17. package/src/plugin.js.map +1 -0
  18. package/srv/blockchain/backends/blockfrost-backend.d.ts.map +1 -0
  19. package/srv/blockchain/backends/blockfrost-backend.js +398 -0
  20. package/srv/blockchain/backends/blockfrost-backend.js.map +1 -0
  21. package/srv/blockchain/backends/cardano-backend.d.ts.map +1 -0
  22. package/srv/blockchain/backends/cardano-backend.js +12 -0
  23. package/srv/blockchain/backends/cardano-backend.js.map +1 -0
  24. package/srv/blockchain/backends/koios-backend.d.ts.map +1 -0
  25. package/srv/blockchain/backends/koios-backend.js +537 -0
  26. package/srv/blockchain/backends/koios-backend.js.map +1 -0
  27. package/srv/blockchain/backends/ogmios-backend.d.ts.map +1 -0
  28. package/srv/blockchain/backends/ogmios-backend.js +516 -0
  29. package/srv/blockchain/backends/ogmios-backend.js.map +1 -0
  30. package/srv/blockchain/cardano-client.d.ts.map +1 -0
  31. package/srv/blockchain/cardano-client.js +377 -0
  32. package/srv/blockchain/cardano-client.js.map +1 -0
  33. package/srv/blockchain/cardano-indexer.d.ts.map +1 -0
  34. package/srv/blockchain/cardano-indexer.js +542 -0
  35. package/srv/blockchain/cardano-indexer.js.map +1 -0
  36. package/srv/blockchain/cardano-tx-builder.d.ts.map +1 -0
  37. package/srv/blockchain/cardano-tx-builder.js +232 -0
  38. package/srv/blockchain/cardano-tx-builder.js.map +1 -0
  39. package/srv/blockchain/circuit-breaker.d.ts.map +1 -0
  40. package/srv/blockchain/circuit-breaker.js +110 -0
  41. package/srv/blockchain/circuit-breaker.js.map +1 -0
  42. package/srv/blockchain/signing/external-signer.d.ts.map +1 -0
  43. package/srv/blockchain/signing/external-signer.js +302 -0
  44. package/srv/blockchain/signing/external-signer.js.map +1 -0
  45. package/srv/blockchain/signing/signature-verifier.d.ts.map +1 -0
  46. package/srv/blockchain/signing/signature-verifier.js +249 -0
  47. package/srv/blockchain/signing/signature-verifier.js.map +1 -0
  48. package/srv/blockchain/transaction-building/buildooor-tx.d.ts.map +1 -0
  49. package/srv/blockchain/transaction-building/buildooor-tx.js +636 -0
  50. package/srv/blockchain/transaction-building/buildooor-tx.js.map +1 -0
  51. package/srv/blockchain/transaction-building/cardano-tx.d.ts.map +1 -0
  52. package/srv/blockchain/transaction-building/cardano-tx.js +3 -0
  53. package/srv/blockchain/transaction-building/cardano-tx.js.map +1 -0
  54. package/srv/blockchain/transaction-building/csl-tx.d.ts.map +1 -0
  55. package/srv/blockchain/transaction-building/csl-tx.js +766 -0
  56. package/srv/blockchain/transaction-building/csl-tx.js.map +1 -0
  57. package/srv/blockchain/transaction-building/tx-builder-registry.d.ts.map +1 -0
  58. package/srv/blockchain/transaction-building/tx-builder-registry.js +67 -0
  59. package/srv/blockchain/transaction-building/tx-builder-registry.js.map +1 -0
  60. package/srv/cardano-service.cds +179 -0
  61. package/srv/cardano-service.d.ts.map +1 -0
  62. package/srv/cardano-service.js +227 -0
  63. package/srv/cardano-service.js.map +1 -0
  64. package/srv/cardano-tx-service.cds +298 -0
  65. package/srv/cardano-tx-service.d.ts.map +1 -0
  66. package/srv/cardano-tx-service.js +646 -0
  67. package/srv/cardano-tx-service.js.map +1 -0
  68. package/srv/cardano-ui.cds +2949 -0
  69. package/srv/server.d.ts.map +1 -0
  70. package/srv/server.js +212 -0
  71. package/srv/server.js.map +1 -0
  72. package/srv/utils/backend-request-handler.d.ts.map +1 -0
  73. package/srv/utils/backend-request-handler.js +47 -0
  74. package/srv/utils/backend-request-handler.js.map +1 -0
  75. package/srv/utils/const.d.ts.map +1 -0
  76. package/srv/utils/const.js +86 -0
  77. package/srv/utils/const.js.map +1 -0
  78. package/srv/utils/error-codes.d.ts.map +1 -0
  79. package/srv/utils/error-codes.js +49 -0
  80. package/srv/utils/error-codes.js.map +1 -0
  81. package/srv/utils/errors.d.ts.map +1 -0
  82. package/srv/utils/errors.js +389 -0
  83. package/srv/utils/errors.js.map +1 -0
  84. package/srv/utils/mappers.d.ts.map +1 -0
  85. package/srv/utils/mappers.js +723 -0
  86. package/srv/utils/mappers.js.map +1 -0
  87. package/srv/utils/signing-helper.d.ts.map +1 -0
  88. package/srv/utils/signing-helper.js +128 -0
  89. package/srv/utils/signing-helper.js.map +1 -0
  90. package/srv/utils/tx-build-helper.d.ts.map +1 -0
  91. package/srv/utils/tx-build-helper.js +135 -0
  92. package/srv/utils/tx-build-helper.js.map +1 -0
  93. package/srv/utils/types.d.ts.map +1 -0
  94. package/srv/utils/types.js +36 -0
  95. package/srv/utils/types.js.map +1 -0
  96. package/srv/utils/validators.d.ts.map +1 -0
  97. package/srv/utils/validators.js +382 -0
  98. package/srv/utils/validators.js.map +1 -0
@@ -0,0 +1,646 @@
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
+ const cds_1 = __importDefault(require("@sap/cds"));
7
+ const backend_request_handler_1 = require("./utils/backend-request-handler");
8
+ const errors_1 = require("./utils/errors");
9
+ const validators_1 = require("./utils/validators");
10
+ const tx_build_helper_1 = require("./utils/tx-build-helper");
11
+ const server_1 = require("./server");
12
+ const external_signer_1 = require("./blockchain/signing/external-signer");
13
+ const signing_helper_1 = require("./utils/signing-helper");
14
+ const { SELECT, UPDATE } = cds_1.default.ql;
15
+ const logger = cds_1.default.log('CardanoTxService');
16
+ /**
17
+ * Check if a signing request has expired and update its status.
18
+ * @returns true if expired, false otherwise
19
+ */
20
+ async function checkAndExpireSigningRequest(db, signingRequest, SigningRequests) {
21
+ if (new Date(signingRequest.expiresAt) < new Date()) {
22
+ await db.run(UPDATE.entity(SigningRequests).set({ status: 'expired' }).where({ id: signingRequest.id }));
23
+ signingRequest.status = 'expired';
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+ /**
29
+ * Cardano Transaction Service Implementation
30
+ * Handles transaction building and submission operations & some additional data queries.
31
+ */
32
+ module.exports = (srv) => {
33
+ logger.info('[CardanoTxService] Module loaded - registering handlers');
34
+ const { TransactionBuilds, TransactionBuildInputs, TransactionBuildOutputs, TransactionSubmissions, TransactionSubmissionErrors, SigningRequests, SignatureVerifications, AddressSigningRequests, AddressTransactionBuilds } = require('#cds-models/CardanoTransactionService');
35
+ /**
36
+ * READ handler for TransactionBuilds entity
37
+ * @param req - The incoming request data
38
+ * @returns {TransactionBuilds} The transaction builds fitting the request query
39
+ */
40
+ srv.on('READ', TransactionBuilds, async (req) => {
41
+ logger.debug('TransactionBuilds READ handler called');
42
+ return (0, backend_request_handler_1.handleRequest)(req, (db) => db.run(req.query));
43
+ });
44
+ /**
45
+ * READ handler for TransactionBuildInputs entity
46
+ * @param req - The incoming request data
47
+ * @returns {TransactionBuildInputs} The transaction build inputs fitting the request query
48
+ */
49
+ srv.on('READ', TransactionBuildInputs, async (req) => {
50
+ logger.debug('TransactionBuildInputs READ handler called');
51
+ return (0, backend_request_handler_1.handleRequest)(req, (db) => db.run(req.query));
52
+ });
53
+ /**
54
+ * READ handler for TransactionBuildOutputs entity
55
+ * @param req - The incoming request data
56
+ * @returns {TransactionBuildOutputs} The transaction build outputs fitting the request query
57
+ */
58
+ srv.on('READ', TransactionBuildOutputs, async (req) => {
59
+ logger.debug('TransactionBuildOutputs READ handler called');
60
+ return (0, backend_request_handler_1.handleRequest)(req, (db) => db.run(req.query));
61
+ });
62
+ /**
63
+ * READ handler for TransactionSubmissions entity
64
+ * @param req - The incoming request data
65
+ * @returns {TransactionSubmissions} The transaction submissions fitting the request query
66
+ */
67
+ srv.on('READ', TransactionSubmissions, async (req) => {
68
+ logger.debug('TransactionSubmissions READ handler called');
69
+ return (0, backend_request_handler_1.handleRequest)(req, (db) => db.run(req.query));
70
+ });
71
+ /**
72
+ * READ handler for TransactionSubmissionErrors entity
73
+ * @param req - The incoming request data
74
+ * @returns {TransactionSubmissionErrors} The transaction submission errors fitting the request query
75
+ */
76
+ srv.on('READ', TransactionSubmissionErrors, async (req) => {
77
+ logger.debug('TransactionSubmissionErrors READ handler called');
78
+ return (0, backend_request_handler_1.handleRequest)(req, (db) => db.run(req.query));
79
+ });
80
+ /**
81
+ * Build a simple ADA-only transaction
82
+ * @param req - CDS request object (with senderAddress, recipientAddress, lovelaceAmount, changeAddress)
83
+ * @returns {TransactionBuild} Transaction build details
84
+ */
85
+ srv.on('BuildSimpleAdaTransaction', async (req) => {
86
+ const { senderAddress, recipientAddress, lovelaceAmount, outputDatumJson } = req.data;
87
+ // validate inputs
88
+ const errors = (0, validators_1.validateTransactionInputs)({ senderAddress, recipientAddress, lovelaceAmount }, ['senderAddress', 'recipientAddress', 'lovelaceAmount']);
89
+ (0, errors_1.throwIfValidationErrors)(req, 'BuildSimpleAdaTransaction', errors);
90
+ // parse optional output datum
91
+ const cleanData = { ...req.data };
92
+ if (outputDatumJson) {
93
+ try {
94
+ cleanData.outputDatum = JSON.parse(outputDatumJson);
95
+ }
96
+ catch {
97
+ return req.reject(400, 'Invalid outputDatumJson: must be valid JSON');
98
+ }
99
+ }
100
+ // handle the request / building the transaction / indexing the build result / returning build details
101
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
102
+ logger.info({ senderAddress, recipientAddress, lovelaceAmount, hasDatum: !!outputDatumJson }, 'Building simple ADA transaction');
103
+ return await (0, server_1.getCardanoIndexer)().indexSimpleBuildResult(db, cleanData);
104
+ });
105
+ });
106
+ /**
107
+ * Build a transaction with metadata
108
+ * @param req - CDS request object (with senderAddress, recipientAddress, lovelaceAmount, metadataJson, changeAddress)
109
+ * @returns {TransactionBuild} Transaction build details
110
+ */
111
+ srv.on('BuildTransactionWithMetadata', async (req) => {
112
+ const { senderAddress, recipientAddress, lovelaceAmount, metadataJson } = req.data;
113
+ // validate inputs (includes JSON parsing validation)
114
+ const errors = (0, validators_1.validateTransactionInputs)({ senderAddress, recipientAddress, lovelaceAmount, metadataJson }, ['senderAddress', 'recipientAddress', 'lovelaceAmount', 'metadataJson']);
115
+ (0, errors_1.throwIfValidationErrors)(req, 'BuildTransactionWithMetadata', errors);
116
+ // Parse metadataJson (already validated as valid JSON)
117
+ const parsedMetadata = JSON.parse(metadataJson);
118
+ // handle the request / building the transaction / indexing the build result / returning build details
119
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
120
+ logger.info({ senderAddress, recipientAddress, lovelaceAmount, metadataJson: parsedMetadata }, 'Building transaction with metadata');
121
+ return await (0, server_1.getCardanoIndexer)().indexMetadataBuildResult(db, { ...req.data, metadataJson: parsedMetadata });
122
+ });
123
+ });
124
+ /**
125
+ * Build a multi-asset transaction
126
+ * @param req - CDS request object (with senderAddress, recipientAddress, lovelaceAmount, assetsJson, changeAddress)
127
+ * @returns {TransactionBuild} Transaction build details
128
+ */
129
+ srv.on('BuildMultiAssetTransaction', async (req) => {
130
+ const { senderAddress, recipientAddress, lovelaceAmount, assetsJson } = req.data;
131
+ // validate inputs (includes JSON parsing validation)
132
+ const errors = (0, validators_1.validateTransactionInputs)({ senderAddress, recipientAddress, lovelaceAmount, assetsJson }, ['senderAddress', 'recipientAddress', 'lovelaceAmount', 'assetsJson']);
133
+ (0, errors_1.throwIfValidationErrors)(req, 'BuildMultiAssetTransaction', errors);
134
+ // Parse assetsJson (already validated as valid JSON by validateTransactionInputs)
135
+ const parsedAssets = JSON.parse(assetsJson);
136
+ if (!Array.isArray(parsedAssets)) {
137
+ (0, errors_1.rejectInvalid)(req, 'BuildMultiAssetTransaction', 'assetsJson must be a JSON array', 'assetsJson');
138
+ }
139
+ // handle the request / building the transaction / indexing the build result / returning build details
140
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
141
+ logger.info({ senderAddress, recipientAddress, lovelaceAmount, assets: parsedAssets }, 'Building multi-asset transaction');
142
+ // Create clean request object with parsed assets (remove assetsJson, add assets)
143
+ const cleanData = { ...req.data };
144
+ delete cleanData.assetsJson;
145
+ const result = await (0, server_1.getCardanoIndexer)().indexMultiAssetBuildResult(db, { ...cleanData, assets: parsedAssets });
146
+ logger.info({ result }, 'Multi-asset transaction build result');
147
+ return result;
148
+ });
149
+ });
150
+ /**
151
+ * Build a minting transaction
152
+ * @param req - CDS request object (with senderAddress, recipientAddress, lovelaceAmount, mintActionsJson, mintingPolicyScript, changeAddress)
153
+ * @returns {TransactionBuild} Transaction build details
154
+ */
155
+ srv.on('BuildMintTransaction', async (req) => {
156
+ const { senderAddress, recipientAddress, lovelaceAmount, mintActionsJson, mintingPolicyScript } = req.data;
157
+ // validate inputs (includes JSON and CBOR validation)
158
+ const errors = (0, validators_1.validateTransactionInputs)({ senderAddress, recipientAddress, mintActionsJson, mintingPolicyScript }, ['senderAddress', 'recipientAddress', 'mintActionsJson', 'mintingPolicyScript']);
159
+ (0, errors_1.throwIfValidationErrors)(req, 'BuildMintTransaction', errors);
160
+ // Parse mintActionsJson and convert quantity strings to bigint
161
+ const parsedMintActionsRaw = JSON.parse(mintActionsJson);
162
+ if (!Array.isArray(parsedMintActionsRaw)) {
163
+ (0, errors_1.rejectInvalid)(req, 'BuildMintTransaction', 'mintActionsJson must be a JSON array', 'mintActionsJson');
164
+ }
165
+ const parsedMintActions = parsedMintActionsRaw.map((action) => ({
166
+ ...action,
167
+ quantity: BigInt(action.quantity)
168
+ }));
169
+ // handle the request / building the transaction / indexing the build result / returning build details
170
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
171
+ logger.info({ senderAddress, recipientAddress, lovelaceAmount, mintActions: parsedMintActions }, 'Building minting transaction');
172
+ // Create clean request object with parsed mintActions (remove mintActionsJson, add mintActions)
173
+ const cleanData = { ...req.data };
174
+ delete cleanData.mintActionsJson;
175
+ return await (0, server_1.getCardanoIndexer)().indexMintBuildResult(db, {
176
+ ...cleanData,
177
+ mintActions: parsedMintActions,
178
+ mintingPolicyScript
179
+ });
180
+ });
181
+ });
182
+ /**
183
+ * Build a Plutus spending transaction (consume UTxO at script address)
184
+ * @param req - CDS request object (with senderAddress, recipientAddress, lovelaceAmount, validatorScript, scriptTxHash, scriptOutputIndex, redeemerJson, datumJson, changeAddress)
185
+ * @returns {TransactionBuild} Transaction build details
186
+ */
187
+ srv.on('BuildPlutusSpendTransaction', async (req) => {
188
+ const { senderAddress, recipientAddress, lovelaceAmount, validatorScript, scriptTxHash, scriptOutputIndex, redeemerJson, datumJson } = req.data;
189
+ // Validate inputs
190
+ const errors = (0, validators_1.validateTransactionInputs)({ senderAddress, recipientAddress, validatorScript, scriptTxHash, scriptOutputIndex, redeemerJson }, ['senderAddress', 'recipientAddress', 'validatorScript', 'scriptTxHash', 'redeemerJson']);
191
+ (0, errors_1.throwIfValidationErrors)(req, 'BuildPlutusSpendTransaction', errors);
192
+ // Validate scriptOutputIndex separately (it's a number, not caught by required-fields check for empty string)
193
+ if (scriptOutputIndex === undefined || scriptOutputIndex === null) {
194
+ (0, errors_1.rejectMissing)(req, 'BuildPlutusSpendTransaction', 'scriptOutputIndex');
195
+ }
196
+ // Parse redeemer JSON
197
+ const parsedRedeemer = JSON.parse(redeemerJson);
198
+ // Parse optional datum JSON
199
+ const parsedDatum = datumJson ? JSON.parse(datumJson) : undefined;
200
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
201
+ logger.info({ senderAddress, recipientAddress, lovelaceAmount, scriptTxHash, scriptOutputIndex }, 'Building Plutus spending transaction');
202
+ const cleanData = {
203
+ ...req.data,
204
+ plutusScriptExecution: {
205
+ validatorScript,
206
+ scriptUtxo: {
207
+ txHash: scriptTxHash,
208
+ outputIndex: scriptOutputIndex,
209
+ },
210
+ redeemer: parsedRedeemer,
211
+ datum: parsedDatum,
212
+ }
213
+ };
214
+ return await (0, server_1.getCardanoIndexer)().indexPlutusSpendBuildResult(db, cleanData);
215
+ });
216
+ });
217
+ /**
218
+ * Get build details for previously built transaction
219
+ * @param req - CDS request object (with buildId)
220
+ * @returns {TransactionBuild} Transaction build details
221
+ */
222
+ srv.on('GetBuildDetails', async (req) => {
223
+ const { buildId } = req.data;
224
+ // Validate inputs
225
+ const errors = (0, validators_1.validateTransactionInputs)({ buildId }, ['buildId']);
226
+ (0, errors_1.throwIfValidationErrors)(req, 'GetBuildDetails', errors);
227
+ // handle the request / fetching the build details
228
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
229
+ const existing = await db.run(SELECT.one.from(TransactionBuilds).where({ id: buildId }));
230
+ if (!existing)
231
+ (0, errors_1.rejectInvalid)(req, 'GetBuildDetails', 'Build not found', 'buildId');
232
+ return existing;
233
+ });
234
+ });
235
+ /**
236
+ * Set up a collateral UTxO for Plutus transactions.
237
+ * Checks if the address already has >= 2 UTxOs with >= 5 ADA each.
238
+ * If not, builds a self-send transaction to create a 5 ADA collateral UTxO.
239
+ * @param req - CDS request object (with address)
240
+ * @returns {TransactionBuild} Transaction build details for the collateral setup
241
+ */
242
+ srv.on('SetCollateral', async (req) => {
243
+ const { address } = req.data;
244
+ if (!address || !(0, validators_1.isValidBech32Address)(address)) {
245
+ return req.reject(400, 'SetCollateral: Invalid or missing Bech32 address');
246
+ }
247
+ const COLLATERAL_LOVELACE = 5000000n;
248
+ const FEE_BUFFER_LOVELACE = 1000000n;
249
+ // Fetch UTxOs and validate before entering handleRequest (req.reject inside handleRequest gets wrapped as 500)
250
+ const utxos = await (0, server_1.getCardanoClient)().getAddressUtxos(address);
251
+ if (utxos.length === 0) {
252
+ return req.reject(400, 'SetCollateral: No UTxOs found at address');
253
+ }
254
+ const qualifyingUtxos = utxos.filter(u => (0, tx_build_helper_1.getLovelace)(u) >= COLLATERAL_LOVELACE);
255
+ if (qualifyingUtxos.length >= 2) {
256
+ return req.reject(409, `SetCollateral: Collateral already available — found ${qualifyingUtxos.length} UTxOs with >= 5 ADA`);
257
+ }
258
+ const totalLovelace = utxos.reduce((sum, u) => sum + (0, tx_build_helper_1.getLovelace)(u), 0n);
259
+ if (totalLovelace < COLLATERAL_LOVELACE + FEE_BUFFER_LOVELACE) {
260
+ return req.reject(400, `SetCollateral: Insufficient funds — need at least ${Number(COLLATERAL_LOVELACE + FEE_BUFFER_LOVELACE) / 1_000_000} ADA, have ${Number(totalLovelace) / 1_000_000} ADA`);
261
+ }
262
+ // Build self-send transaction: address → address, 5 ADA
263
+ logger.info({ address, existingQualifying: qualifyingUtxos.length }, 'Building collateral setup transaction');
264
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
265
+ return await (0, server_1.getCardanoIndexer)().indexSimpleBuildResult(db, {
266
+ network: (0, server_1.getCardanoClient)().network,
267
+ senderAddress: address,
268
+ recipientAddress: address,
269
+ lovelaceAmount: Number(COLLATERAL_LOVELACE),
270
+ changeAddress: address,
271
+ });
272
+ });
273
+ });
274
+ /**
275
+ * Submit signed transaction built previously
276
+ * Handler validates, checks build exists, submits to blockchain, delegates persistence to indexer
277
+ * @param req - CDS request object (with buildId, signedTxCbor)
278
+ * @returns {TransactionSubmission} Transaction submission details
279
+ */
280
+ srv.on('SubmitTransaction', async (req) => {
281
+ logger.debug('SubmitTransaction Action handler called');
282
+ const { buildId, signedTxCbor } = req.data;
283
+ // Validate inputs (includes CBOR format validation)
284
+ const errors = (0, validators_1.validateTransactionInputs)({ buildId, signedTxCbor }, ['buildId', 'signedTxCbor']);
285
+ (0, errors_1.throwIfValidationErrors)(req, 'SubmitTransaction', errors);
286
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
287
+ logger.debug({ buildId }, 'Submitting signed transaction');
288
+ // Validate build exists
289
+ const existing = await db.run(SELECT.one.from(TransactionBuilds).where({ id: buildId }));
290
+ if (!existing)
291
+ (0, errors_1.rejectInvalid)(req, 'SubmitTransaction', 'Build not found', 'buildId');
292
+ // Use txBodyHash from build
293
+ const txHash = existing.txBodyHash;
294
+ // Two-phase submit: persist with 'pending', then submit to blockchain
295
+ const submissionRecord = await (0, server_1.getCardanoIndexer)().persistTransactionSubmission(db, {
296
+ signedTxCbor,
297
+ txHash,
298
+ buildId,
299
+ });
300
+ try {
301
+ await (0, server_1.getCardanoClient)().submitTransaction(signedTxCbor);
302
+ logger.info({ txHash }, 'Transaction submitted to blockchain');
303
+ await (0, server_1.getCardanoIndexer)().updateSubmissionStatus(db, submissionRecord.id, 'submitted');
304
+ submissionRecord.status = 'submitted';
305
+ }
306
+ catch (err) {
307
+ logger.error({ txHash, error: err.message }, 'Transaction submission failed');
308
+ await (0, server_1.getCardanoIndexer)().updateSubmissionStatus(db, submissionRecord.id, 'failed', err.message);
309
+ throw err;
310
+ }
311
+ return submissionRecord;
312
+ });
313
+ });
314
+ /**
315
+ * Submit signed transaction without prior build
316
+ * Handler validates, submits to blockchain, delegates persistence to indexer
317
+ * Two-phase: persist with 'pending' first, then update to 'submitted' or 'failed'
318
+ * @param req - CDS request object (with signedTxCbor, network)
319
+ * @returns {TransactionSubmission} Transaction submission details
320
+ */
321
+ srv.on('SubmitSignedTransaction', async (req) => {
322
+ logger.info('SubmitSignedTransaction Action handler called');
323
+ const { signedTxCbor } = req.data;
324
+ // Validate inputs (includes CBOR format validation)
325
+ const errors = (0, validators_1.validateTransactionInputs)({ signedTxCbor }, ['signedTxCbor']);
326
+ (0, errors_1.throwIfValidationErrors)(req, 'SubmitSignedTransaction', errors);
327
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
328
+ // Extract txHash from signed CBOR
329
+ const txHash = (0, tx_build_helper_1.getTxHashFromCbor)(signedTxCbor);
330
+ // Two-phase submit: persist with 'pending', then submit to blockchain
331
+ const submissionRecord = await (0, server_1.getCardanoIndexer)().persistTransactionSubmission(db, {
332
+ signedTxCbor,
333
+ txHash,
334
+ buildId: null,
335
+ });
336
+ try {
337
+ await (0, server_1.getCardanoClient)().submitTransaction(signedTxCbor);
338
+ logger.info({ txHash }, 'External transaction submitted');
339
+ await (0, server_1.getCardanoIndexer)().updateSubmissionStatus(db, submissionRecord.id, 'submitted');
340
+ submissionRecord.status = 'submitted';
341
+ }
342
+ catch (err) {
343
+ logger.error({ txHash, error: err.message }, 'External transaction submission failed');
344
+ await (0, server_1.getCardanoIndexer)().updateSubmissionStatus(db, submissionRecord.id, 'failed', err.message);
345
+ throw err;
346
+ }
347
+ return submissionRecord;
348
+ });
349
+ });
350
+ /**
351
+ * Check submission status (bound action on TransactionSubmissions)
352
+ * @flow.status validates @from: [#submitted] automatically (409 if wrong state)
353
+ * Queries blockchain for transaction confirmation and updates status accordingly
354
+ * @param req - CDS request with entity key in params
355
+ * @returns {TransactionSubmission} The updated transaction submission status
356
+ */
357
+ srv.on('CheckSubmissionStatus', async (req) => {
358
+ logger.debug('CheckSubmissionStatus Action handler called');
359
+ const { id: submissionId } = req.params[0];
360
+ // @from: [#submitted] validated by framework — no manual status check needed
361
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
362
+ const submission = await db.run(SELECT.one.from(TransactionSubmissions).where({ id: submissionId }));
363
+ if (!submission)
364
+ (0, errors_1.rejectInvalid)(req, 'CheckSubmissionStatus', 'Submission not found', 'submissionId');
365
+ // Query blockchain for confirmation
366
+ try {
367
+ const txDetails = await (0, server_1.getCardanoClient)().getTransaction(submission.txHash);
368
+ if (txDetails) {
369
+ await db.run(UPDATE.entity(TransactionSubmissions)
370
+ .set({ status: 'confirmed' })
371
+ .where({ id: submissionId }));
372
+ submission.status = 'confirmed';
373
+ logger.info({ submissionId, txHash: submission.txHash }, 'Transaction confirmed on chain');
374
+ }
375
+ }
376
+ catch {
377
+ // Transaction not yet confirmed on chain — status stays 'submitted'
378
+ logger.debug({ submissionId, txHash: submission.txHash }, 'Transaction not yet confirmed on chain');
379
+ }
380
+ return submission;
381
+ });
382
+ });
383
+ // ---------------------------------------------------------------------------
384
+ // M3 - External Signing Workflow Actions
385
+ // ---------------------------------------------------------------------------
386
+ /**
387
+ * before-READ handler for SigningRequests: lazy expiration check
388
+ * Updates status to 'expired' if expiresAt has passed (for pending requests)
389
+ */
390
+ srv.before('READ', SigningRequests, async (req) => {
391
+ // Expiration check runs on single-entity reads (where ID is provided)
392
+ if (req.params && req.params.length > 0) {
393
+ const { id } = req.params[0];
394
+ if (id) {
395
+ const db = await cds_1.default.connect.to('db');
396
+ const signingRequest = await db.run(SELECT.one.from(SigningRequests).where({ id }));
397
+ if (signingRequest && signingRequest.status === 'pending') {
398
+ await checkAndExpireSigningRequest(db, signingRequest, SigningRequests);
399
+ }
400
+ }
401
+ }
402
+ });
403
+ /**
404
+ * READ handler for SigningRequests entity
405
+ * @param req - The incoming request data
406
+ * @returns {SigningRequest} The signing requests fitting the request query
407
+ */
408
+ srv.on('READ', SigningRequests, async (req) => {
409
+ logger.debug('SigningRequests READ handler called');
410
+ return (0, backend_request_handler_1.handleRequest)(req, (db) => db.run(req.query));
411
+ });
412
+ /**
413
+ * READ handler for SignatureVerifications entity
414
+ * @param req - The incoming request data
415
+ * @returns {SignatureVerification} The signature verifications fitting the request query
416
+ */
417
+ srv.on('READ', SignatureVerifications, async (req) => {
418
+ logger.debug('SignatureVerifications READ handler called');
419
+ return (0, backend_request_handler_1.handleRequest)(req, (db) => db.run(req.query));
420
+ });
421
+ /**
422
+ * READ handler for AddressSigningRequests entity
423
+ * @param req - The incoming request data
424
+ * @returns {AddressSigningRequest} The address signing requests fitting the request query
425
+ */
426
+ srv.on('READ', AddressSigningRequests, async (req) => {
427
+ logger.debug('AddressSigningRequests READ handler called');
428
+ return (0, backend_request_handler_1.handleRequest)(req, (db) => db.run(req.query));
429
+ });
430
+ /**
431
+ * READ handler for AddressTransactionBuilds entity
432
+ * @param req - The incoming request data
433
+ * @returns {AddressTransactionBuild} The address transaction builds fitting the request query
434
+ */
435
+ srv.on('READ', AddressTransactionBuilds, async (req) => {
436
+ logger.debug('AddressTransactionBuilds READ handler called');
437
+ return (0, backend_request_handler_1.handleRequest)(req, (db) => db.run(req.query));
438
+ });
439
+ /**
440
+ * Create a new signing request for external signing
441
+ * Persists the request for audit trail and workflow tracking
442
+ * @param req - CDS request object (with buildId)
443
+ * @returns {SigningRequest} Signing request entity
444
+ */
445
+ srv.on('CreateSigningRequest', async (req) => {
446
+ logger.debug('CreateSigningRequest Action handler called');
447
+ const { buildId, message } = req.data;
448
+ // Validate inputs
449
+ const errors = (0, validators_1.validateTransactionInputs)({ buildId }, ['buildId']);
450
+ (0, errors_1.throwIfValidationErrors)(req, 'CreateSigningRequest', errors);
451
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
452
+ // Fetch the build
453
+ const build = await db.run(SELECT.one.from(TransactionBuilds).where({ id: buildId }));
454
+ if (!build)
455
+ (0, errors_1.rejectInvalid)(req, 'CreateSigningRequest', 'Build not found', 'buildId');
456
+ // Check if signing request already exists for this build
457
+ const existingRequest = await db.run(SELECT.one.from(SigningRequests).where({ build_id: buildId, status: 'pending' }));
458
+ if (existingRequest) {
459
+ logger.info({ buildId, signingRequestId: existingRequest.id }, 'Returning existing signing request');
460
+ return existingRequest;
461
+ }
462
+ // Create signing request using external signer module
463
+ const signerModule = (0, external_signer_1.getExternalSignerModule)();
464
+ const signingPayload = signerModule.createSigningRequest(build.id, build.unsignedTxCbor, build.txBodyHash, build.network, message);
465
+ // Delegate persistence to indexer
466
+ const signingRequestRecord = await (0, server_1.getCardanoIndexer)().persistSigningRequest(db, {
467
+ buildId,
468
+ signingPayload,
469
+ });
470
+ logger.info({ buildId, signingRequestId: signingRequestRecord.id }, 'Created signing request');
471
+ return signingRequestRecord;
472
+ });
473
+ });
474
+ /**
475
+ * Get an existing signing request by ID
476
+ * @param req - CDS request object (with signingRequestId)
477
+ * @returns {SigningRequest} signing request entity
478
+ */
479
+ srv.on('GetSigningRequest', async (req) => {
480
+ logger.debug('GetSigningRequest Action handler called');
481
+ const { signingRequestId } = req.data;
482
+ // Validate inputs
483
+ const errors = (0, validators_1.validateTransactionInputs)({ signingRequestId }, ['signingRequestId']);
484
+ (0, errors_1.throwIfValidationErrors)(req, 'GetSigningRequest', errors);
485
+ // Fetch the signing request within transaction context
486
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
487
+ const signingRequest = await db.run(SELECT.one.from(SigningRequests).where({ id: signingRequestId }));
488
+ if (!signingRequest)
489
+ (0, errors_1.rejectInvalid)(req, 'GetSigningRequest', 'Signing request not found', 'signingRequestId');
490
+ // Check if expired and update status if needed
491
+ if (signingRequest.status === 'pending') {
492
+ await checkAndExpireSigningRequest(db, signingRequest, SigningRequests);
493
+ }
494
+ return signingRequest;
495
+ });
496
+ });
497
+ /**
498
+ * Verify signature of a signed transaction (bound action on SigningRequests)
499
+ * @flow.status validates @from: [#pending] automatically (409 if wrong state)
500
+ * @param req - CDS request with entity key in params, action data in data
501
+ * @returns {SignatureVerification} Persisted signature verification entity
502
+ */
503
+ srv.on('VerifySignature', async (req) => {
504
+ logger.debug('VerifySignature Action handler called');
505
+ const { id: signingRequestId } = req.params[0];
506
+ const { signedTxCbor, signerType, signerInfo } = req.data;
507
+ // Validate inputs (signingRequestId no longer needed — comes from URL)
508
+ const errors = (0, validators_1.validateTransactionInputs)({ signedTxCbor }, ['signedTxCbor']);
509
+ (0, errors_1.throwIfValidationErrors)(req, 'VerifySignature', errors);
510
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
511
+ // Fetch the signing request (@from already validated by framework)
512
+ const signingRequest = await db.run(SELECT.one.from(SigningRequests).where({ id: signingRequestId }));
513
+ if (!signingRequest)
514
+ (0, errors_1.rejectInvalid)(req, 'VerifySignature', 'Signing request not found', 'signingRequestId');
515
+ // Check if expired (time-based, not covered by @flow.status)
516
+ if (await checkAndExpireSigningRequest(db, signingRequest, SigningRequests)) {
517
+ (0, errors_1.rejectInvalid)(req, 'VerifySignature', 'Signing request has expired', 'signingRequestId');
518
+ }
519
+ // Verify the signature
520
+ const signerModule = (0, external_signer_1.getExternalSignerModule)();
521
+ const result = signerModule.verifySignedTransaction(signedTxCbor, signingRequest.txBodyHash);
522
+ // Delegate persistence to indexer
523
+ const verificationRecord = await (0, server_1.getCardanoIndexer)().persistSignatureVerification(db, {
524
+ signingRequestId,
525
+ signedTxCbor,
526
+ verificationResult: result,
527
+ signerType,
528
+ signerInfo,
529
+ });
530
+ logger.info({
531
+ signingRequestId,
532
+ verificationId: verificationRecord.id,
533
+ isValid: result.isValid,
534
+ witnessCount: result.witnessCount,
535
+ }, 'Signature verification completed');
536
+ return verificationRecord;
537
+ });
538
+ });
539
+ /**
540
+ * Verify and submit a signed transaction in one step (bound action on SigningRequests)
541
+ * @flow.status validates @from: [#pending, #verified] and sets @to: #submitted automatically
542
+ * @param req - CDS request with entity key in params, action data in data
543
+ * @returns {TransactionSubmission} Transaction submission details
544
+ */
545
+ srv.on('SubmitVerifiedTransaction', async (req) => {
546
+ logger.debug('SubmitVerifiedTransaction Action handler called');
547
+ const { id: signingRequestId } = req.params[0];
548
+ const { signedTxCbor, signerType, signerInfo } = req.data;
549
+ // Validate inputs (signingRequestId no longer needed — comes from URL)
550
+ const errors = (0, validators_1.validateTransactionInputs)({ signedTxCbor }, ['signedTxCbor']);
551
+ (0, errors_1.throwIfValidationErrors)(req, 'SubmitVerifiedTransaction', errors);
552
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
553
+ // Fetch the signing request (@from already validated by framework)
554
+ const signingRequest = await db.run(SELECT.one.from(SigningRequests).where({ id: signingRequestId }));
555
+ if (!signingRequest)
556
+ (0, errors_1.rejectInvalid)(req, 'SubmitVerifiedTransaction', 'Signing request not found', 'signingRequestId');
557
+ // Check if expired (time-based, not covered by @flow.status)
558
+ if (await checkAndExpireSigningRequest(db, signingRequest, SigningRequests)) {
559
+ (0, errors_1.rejectInvalid)(req, 'SubmitVerifiedTransaction', 'Signing request has expired', 'signingRequestId');
560
+ }
561
+ // STATUS CHECKS REMOVED — @flow.status handles @from: [#pending, #verified]
562
+ // Previously: manual checks for 'submitted', 'expired', 'failed' states
563
+ // Check if build association exists
564
+ if (!signingRequest.build_id) {
565
+ (0, errors_1.rejectInvalid)(req, 'SubmitVerifiedTransaction', 'Signing request has no associated build', 'signingRequestId');
566
+ }
567
+ // Detect if signedTxCbor is a witness set (CIP-30) or a full signed transaction (cardano-cli)
568
+ let fullSignedTxCbor;
569
+ if ((0, signing_helper_1.isWitnessSetCbor)(signedTxCbor)) {
570
+ // CIP-30 wallet returns only witness set - combine with unsigned tx
571
+ fullSignedTxCbor = (0, signing_helper_1.combineTransactionWithWitnesses)(signingRequest.unsignedTxCbor, signedTxCbor);
572
+ logger.debug({ signingRequestId }, 'Combined witness set with unsigned transaction');
573
+ }
574
+ else {
575
+ // Full signed transaction provided (e.g., from cardano-cli)
576
+ fullSignedTxCbor = signedTxCbor;
577
+ logger.debug({ signingRequestId }, 'Using full signed transaction directly');
578
+ }
579
+ // Verify signature (throws on failure)
580
+ const signerModule = (0, external_signer_1.getExternalSignerModule)();
581
+ const verificationResult = signerModule.verifyOrThrow(fullSignedTxCbor, signingRequest.txBodyHash);
582
+ logger.info({
583
+ signingRequestId,
584
+ witnessCount: verificationResult.witnessCount,
585
+ signers: verificationResult.signerKeyHashes,
586
+ }, 'Signature verified, proceeding with submission');
587
+ // Submit to blockchain
588
+ const txHash = signingRequest.txBodyHash;
589
+ await (0, server_1.getCardanoClient)().submitTransaction(fullSignedTxCbor);
590
+ logger.info({ txHash }, 'Verified transaction submitted to blockchain');
591
+ // Delegate all persistence to indexer
592
+ const submissionRecord = await (0, server_1.getCardanoIndexer)().indexVerifiedTransactionSubmission(db, {
593
+ signingRequestId,
594
+ buildId: signingRequest.build_id,
595
+ fullSignedTxCbor,
596
+ txHash,
597
+ verificationResult,
598
+ signerType,
599
+ signerInfo,
600
+ });
601
+ logger.info({
602
+ signingRequestId,
603
+ submissionId: submissionRecord.id,
604
+ txHash,
605
+ }, 'Transaction submitted and all records updated');
606
+ return submissionRecord;
607
+ });
608
+ });
609
+ /**
610
+ * Get all existing signing requests by address
611
+ * @param req - CDS request object (with address)
612
+ * @returns {AddressSigningRequests} Address signing request associations
613
+ */
614
+ srv.on('GetSigningRequestsByAddress', async (req) => {
615
+ logger.debug('GetSigningRequestsByAddress Action handler called');
616
+ const { address } = req.data;
617
+ // Validate input before business logic
618
+ if (!address)
619
+ (0, errors_1.rejectMissing)(req, 'GetSigningRequestsByAddress', 'address');
620
+ if (!(0, validators_1.isValidBech32Address)(address))
621
+ (0, errors_1.rejectInvalid)(req, 'GetSigningRequestsByAddress', 'Invalid bech32 address format', 'address');
622
+ // Fetch the address-signing request associations
623
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
624
+ return db.run(SELECT.from(AddressSigningRequests).where({ address_address: address }));
625
+ });
626
+ });
627
+ /**
628
+ * Get all existing transaction builds by address
629
+ * @param req - CDS request object (with address)
630
+ * @returns {AddressTransactionBuilds} Address transaction build associations
631
+ */
632
+ srv.on('GetTransactionBuildsByAddress', async (req) => {
633
+ logger.debug('GetTransactionBuildsByAddress Action handler called');
634
+ const { address } = req.data;
635
+ // Validate inputs
636
+ if (!address)
637
+ (0, errors_1.rejectMissing)(req, 'GetTransactionBuildsByAddress', 'address');
638
+ if (!(0, validators_1.isValidBech32Address)(address))
639
+ (0, errors_1.rejectInvalid)(req, 'GetTransactionBuildsByAddress', 'Invalid bech32 address format', 'address');
640
+ // Fetch the address-build associations
641
+ return (0, backend_request_handler_1.handleRequest)(req, async (db) => {
642
+ return db.run(SELECT.from(AddressTransactionBuilds).where({ address_address: address }));
643
+ });
644
+ });
645
+ };
646
+ //# sourceMappingURL=cardano-tx-service.js.map