Skip to content

Commit 1056dbe

Browse files
Merge pull request #1299 from input-output-hk/feat/cad-5432-add-initial-plutus-script-support-js-sdk
Feat/cad 5432 add initial plutus script support js sdk
2 parents 4684260 + 936351e commit 1056dbe

File tree

54 files changed

+2545
-354
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2545
-354
lines changed

packages/cardano-services/src/ChainHistory/DbSyncChainHistory/ChainHistoryBuilder.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import {
88
PoolRegisterCertModel,
99
PoolRetireCertModel,
1010
RedeemerModel,
11+
ScriptModel,
1112
StakeCertModel,
1213
TransactionDataMap,
1314
TxIdModel,
1415
TxInput,
1516
TxInputModel,
1617
TxOutMultiAssetModel,
18+
TxOutScriptMap,
1719
TxOutTokenMap,
1820
TxOutput,
1921
TxOutputModel,
@@ -28,6 +30,7 @@ import { Pool, QueryResult } from 'pg';
2830
import { Range, hexStringToBuffer } from '@cardano-sdk/util';
2931
import {
3032
mapCertificate,
33+
mapPlutusScript,
3134
mapRedeemer,
3235
mapTxId,
3336
mapTxInModel,
@@ -68,6 +71,28 @@ export class ChainHistoryBuilder {
6871
return mapTxOutTokenMap(result.rows);
6972
}
7073

74+
public async queryReferenceScriptsByTxOut(txOutModel: TxOutputModel[]): Promise<TxOutScriptMap> {
75+
const txScriptMap: TxOutScriptMap = new Map();
76+
77+
for (const model of txOutModel) {
78+
if (model.reference_script_id) {
79+
const result: QueryResult<ScriptModel> = await this.#db.query({
80+
name: 'tx_reference_scripts_by_tx_out_ids',
81+
text: Queries.findReferenceScriptsById,
82+
values: [[model.reference_script_id]]
83+
});
84+
85+
if (result.rows.length === 0) continue;
86+
if (result.rows[0].type === 'timelock') continue; // Shouldn't happen.
87+
88+
// There can only be one refScript per output.
89+
txScriptMap.set(model.id, mapPlutusScript(result.rows[0]));
90+
}
91+
}
92+
93+
return txScriptMap;
94+
}
95+
7196
public async queryTransactionOutputsByIds(ids: string[], collateral = false): Promise<TxOutput[]> {
7297
this.#logger.debug(`About to find outputs (collateral: ${collateral}) for transactions with ids:`, ids);
7398
const result: QueryResult<TxOutputModel> = await this.#db.query({
@@ -79,7 +104,14 @@ export class ChainHistoryBuilder {
79104

80105
const txOutIds = result.rows.flatMap((txOut) => BigInt(txOut.id));
81106
const multiAssets = await this.queryMultiAssetsByTxOut(txOutIds);
82-
return result.rows.map((txOut) => mapTxOutModel(txOut, multiAssets.get(txOut.id)));
107+
const referenceScripts = await this.queryReferenceScriptsByTxOut(result.rows);
108+
109+
return result.rows.map((txOut) =>
110+
mapTxOutModel(txOut, {
111+
assets: multiAssets.get(txOut.id),
112+
script: referenceScripts.get(txOut.id)
113+
})
114+
);
83115
}
84116

85117
public async queryTxMintByIds(ids: string[]): Promise<TxTokenMap> {

packages/cardano-services/src/ChainHistory/DbSyncChainHistory/mappers.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as Crypto from '@cardano-sdk/crypto';
2-
import { BigIntMath } from '@cardano-sdk/util';
2+
import { BigIntMath, HexBlob } from '@cardano-sdk/util';
33
import {
44
BlockModel,
55
BlockOutputModel,
66
CertificateModel,
77
MultiAssetModel,
88
RedeemerModel,
9+
ScriptModel,
910
TipModel,
1011
TxIdModel,
1112
TxInput,
@@ -84,14 +85,18 @@ export const mapTxOut = (txOut: TxOutput): Cardano.TxOut => ({
8485
value: txOut.value
8586
});
8687

87-
export const mapTxOutModel = (txOutModel: TxOutputModel, assets?: Cardano.TokenMap): TxOutput => ({
88+
export const mapTxOutModel = (
89+
txOutModel: TxOutputModel,
90+
props: { assets?: Cardano.TokenMap; script?: Cardano.Script }
91+
): TxOutput => ({
8892
address: txOutModel.address as unknown as Cardano.PaymentAddress,
8993
// Inline datums are missing, but for now it's ok on ChainHistoryProvider
9094
datumHash: txOutModel.datum ? (txOutModel.datum.toString('hex') as unknown as Hash32ByteBase16) : undefined,
9195
index: txOutModel.index,
96+
scriptReference: props.script,
9297
txId: txOutModel.tx_id.toString('hex') as unknown as Cardano.TransactionId,
9398
value: {
94-
assets: assets && assets.size > 0 ? assets : undefined,
99+
assets: props.assets && props.assets.size > 0 ? props.assets : undefined,
95100
coins: BigInt(txOutModel.coin_value)
96101
}
97102
});
@@ -101,6 +106,21 @@ export const mapWithdrawal = (withdrawalModel: WithdrawalModel): Cardano.Withdra
101106
stakeAddress: withdrawalModel.stake_address as unknown as Cardano.RewardAccount
102107
});
103108

109+
export const mapPlutusScript = (scriptModel: ScriptModel): Cardano.Script => {
110+
const cbor = scriptModel.bytes.toString('hex') as HexBlob;
111+
112+
return {
113+
__type: Cardano.ScriptType.Plutus,
114+
bytes: cbor,
115+
version:
116+
scriptModel.type === 'plutusV1'
117+
? Cardano.PlutusLanguageVersion.V1
118+
: scriptModel.type === 'plutusV2'
119+
? Cardano.PlutusLanguageVersion.V2
120+
: Cardano.PlutusLanguageVersion.V3
121+
};
122+
};
123+
104124
// TODO: unfortunately this is not nullable and not implemented.
105125
// Remove this and select the actual redeemer data from `redeemer_data` table.
106126
const stubRedeemerData = Buffer.from('not implemented');

packages/cardano-services/src/ChainHistory/DbSyncChainHistory/queries.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const selectTxOutput = (collateral = false) => `
2222
tx_out."index" AS "index",
2323
tx_out.value AS coin_value,
2424
tx_out.data_hash AS datum,
25+
tx_out.reference_script_id as reference_script_id,
2526
tx.hash AS tx_id
2627
FROM ${collateral ? 'collateral_tx_out' : 'tx_out'} AS tx_out
2728
JOIN tx ON tx_out.tx_id = tx.id`;
@@ -120,6 +121,14 @@ export const findMultiAssetByTxOut = `
120121
WHERE tx_out.id = ANY($1)
121122
ORDER BY ma_out.id ASC`;
122123

124+
export const findReferenceScriptsById = `
125+
SELECT
126+
script.type AS type,
127+
script.bytes AS bytes,
128+
script.serialised_size AS serialized_size
129+
FROM script AS script
130+
WHERE id = ANY($1)`;
131+
123132
export const findTxMintByIds = `
124133
SELECT
125134
mint.quantity AS quantity,

packages/cardano-services/src/ChainHistory/DbSyncChainHistory/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Cardano } from '@cardano-sdk/core';
33
export type TransactionDataMap<T> = Map<Cardano.TransactionId, T>;
44
export type TxOutTokenMap = Map<string, Cardano.TokenMap>;
55
export type TxTokenMap = TransactionDataMap<Cardano.TokenMap>;
6+
export type TxOutScriptMap = Map<string, Cardano.Script>;
67

78
export interface BlockModel {
89
block_no: number;
@@ -67,6 +68,7 @@ export interface TxOutputModel {
6768
datum?: Buffer | null;
6869
id: string;
6970
index: number;
71+
reference_script_id: number | null;
7072
tx_id: Buffer;
7173
}
7274

@@ -87,6 +89,13 @@ export interface TxOutMultiAssetModel extends MultiAssetModel {
8789
tx_out_id: string;
8890
}
8991

92+
export interface ScriptModel {
93+
type: 'timelock' | 'plutusV1' | 'plutusV2' | 'plutusV3';
94+
bytes: Buffer;
95+
hash: Buffer;
96+
serialised_size: number;
97+
}
98+
9099
export interface WithdrawalModel {
91100
quantity: string;
92101
tx_id: Buffer;

packages/cardano-services/test/ChainHistory/ChainHistoryHttpService.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,15 @@ describe('ChainHistoryHttpService', () => {
313313
expect(tx.auxiliaryData).toBeDefined();
314314
});
315315

316+
it('has script reference data', async () => {
317+
const response = await provider.transactionsByHashes({
318+
ids: await fixtureBuilder.getTxHashes(1, { with: [TxWith.ScriptReference] })
319+
});
320+
const tx: Cardano.HydratedTx = response[0];
321+
expect(response.length).toEqual(1);
322+
expect(tx.body.outputs.some((txOut) => !!txOut.scriptReference)).toBeTruthy();
323+
});
324+
316325
it('has collateral inputs', async () => {
317326
const response = await provider.transactionsByHashes({
318327
ids: await fixtureBuilder.getTxHashes(1, { with: [TxWith.CollateralInput] })

packages/cardano-services/test/ChainHistory/DbSyncChainHistoryProvider/mappers.test.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from '../../../src/ChainHistory/DbSyncChainHistory/types';
2727
import { Cardano } from '@cardano-sdk/core';
2828
import { Hash28ByteBase16, Hash32ByteBase16 } from '@cardano-sdk/crypto';
29+
import { HexBlob } from '@cardano-sdk/util';
2930

3031
const blockHash = '7a48b034645f51743550bbaf81f8a14771e58856e031eb63844738ca8ad72298';
3132
const poolId = 'pool1zuevzm3xlrhmwjw87ec38mzs02tlkwec9wxpgafcaykmwg7efhh';
@@ -77,12 +78,22 @@ const txOutModel: TxOutputModel = {
7778
datum: Buffer.from(hash32ByteBase16, 'hex'),
7879
id: '1',
7980
index: 1,
81+
reference_script_id: 0,
8082
tx_id: Buffer.from(transactionHash, 'hex')
8183
};
8284
const assets: Cardano.TokenMap = new Map([
8385
[AssetId.TSLA, 500n],
8486
[AssetId.PXL, 500n]
8587
]);
88+
89+
const script: Cardano.PlutusScript = {
90+
__type: Cardano.ScriptType.Plutus,
91+
bytes: HexBlob(
92+
'59079201000033232323232323232323232323232332232323232323232222232325335333006300800530070043333573466E1CD55CEA80124000466442466002006004646464646464646464646464646666AE68CDC39AAB9D500C480008CCCCCCCCCCCC88888888888848CCCCCCCCCCCC00403403002C02802402001C01801401000C008CD4060064D5D0A80619A80C00C9ABA1500B33501801A35742A014666AA038EB9406CD5D0A804999AA80E3AE501B35742A01066A0300466AE85401CCCD54070091D69ABA150063232323333573466E1CD55CEA801240004664424660020060046464646666AE68CDC39AAB9D5002480008CC8848CC00400C008CD40B9D69ABA15002302F357426AE8940088C98C80C8CD5CE01981901809AAB9E5001137540026AE854008C8C8C8CCCD5CD19B8735573AA004900011991091980080180119A8173AD35742A004605E6AE84D5D1280111931901919AB9C033032030135573CA00226EA8004D5D09ABA2500223263202E33573805E05C05826AAE7940044DD50009ABA1500533501875C6AE854010CCD540700808004D5D0A801999AA80E3AE200135742A00460446AE84D5D1280111931901519AB9C02B02A028135744A00226AE8940044D5D1280089ABA25001135744A00226AE8940044D5D1280089ABA25001135744A00226AE8940044D55CF280089BAA00135742A00460246AE84D5D1280111931900E19AB9C01D01C01A101B13263201B3357389201035054350001B135573CA00226EA80054049404448C88C008DD6000990009AA80A911999AAB9F0012500A233500930043574200460066AE880080548C8C8CCCD5CD19B8735573AA004900011991091980080180118061ABA150023005357426AE8940088C98C8054CD5CE00B00A80989AAB9E5001137540024646464646666AE68CDC39AAB9D5004480008CCCC888848CCCC00401401000C008C8C8C8CCCD5CD19B8735573AA0049000119910919800801801180A9ABA1500233500F014357426AE8940088C98C8068CD5CE00D80D00C09AAB9E5001137540026AE854010CCD54021D728039ABA150033232323333573466E1D4005200423212223002004357426AAE79400C8CCCD5CD19B875002480088C84888C004010DD71ABA135573CA00846666AE68CDC3A801A400042444006464C6403866AE700740700680640604D55CEA80089BAA00135742A00466A016EB8D5D09ABA2500223263201633573802E02C02826AE8940044D5D1280089AAB9E500113754002266AA002EB9D6889119118011BAB00132001355012223233335573E0044A010466A00E66442466002006004600C6AAE754008C014D55CF280118021ABA200301313574200222440042442446600200800624464646666AE68CDC3A800A40004642446004006600A6AE84D55CF280191999AB9A3370EA0049001109100091931900899AB9C01201100F00E135573AA00226EA80048C8C8CCCD5CD19B875001480188C848888C010014C01CD5D09AAB9E500323333573466E1D400920042321222230020053009357426AAE7940108CCCD5CD19B875003480088C848888C004014C01CD5D09AAB9E500523333573466E1D40112000232122223003005375C6AE84D55CF280311931900899AB9C01201100F00E00D00C135573AA00226EA80048C8C8CCCD5CD19B8735573AA004900011991091980080180118029ABA15002375A6AE84D5D1280111931900699AB9C00E00D00B135573CA00226EA80048C8CCCD5CD19B8735573AA002900011BAE357426AAE7940088C98C802CCD5CE00600580489BAA001232323232323333573466E1D4005200C21222222200323333573466E1D4009200A21222222200423333573466E1D400D2008233221222222233001009008375C6AE854014DD69ABA135744A00A46666AE68CDC3A8022400C4664424444444660040120106EB8D5D0A8039BAE357426AE89401C8CCCD5CD19B875005480108CC8848888888CC018024020C030D5D0A8049BAE357426AE8940248CCCD5CD19B875006480088C848888888C01C020C034D5D09AAB9E500B23333573466E1D401D2000232122222223005008300E357426AAE7940308C98C8050CD5CE00A80A00900880800780700680609AAB9D5004135573CA00626AAE7940084D55CF280089BAA0012323232323333573466E1D400520022333222122333001005004003375A6AE854010DD69ABA15003375A6AE84D5D1280191999AB9A3370EA0049000119091180100198041ABA135573CA00C464C6401A66AE7003803402C0284D55CEA80189ABA25001135573CA00226EA80048C8C8CCCD5CD19B875001480088C8488C00400CDD71ABA135573CA00646666AE68CDC3A8012400046424460040066EB8D5D09AAB9E500423263200A33573801601401000E26AAE7540044DD500089119191999AB9A3370EA00290021091100091999AB9A3370EA00490011190911180180218031ABA135573CA00846666AE68CDC3A801A400042444004464C6401666AE7003002C02402001C4D55CEA80089BAA0012323333573466E1D40052002212200223333573466E1D40092000212200123263200733573801000E00A00826AAE74DD5000891999AB9A3370E6AAE74DD5000A40004008464C6400866AE700140100092612001490103505431001123230010012233003300200200122212200201'
93+
),
94+
version: Cardano.PlutusLanguageVersion.V2
95+
};
96+
8697
const multiAssetModel: MultiAssetModel = {
8798
asset_name: Buffer.from(assetName, 'hex'),
8899
fingerprint,
@@ -459,7 +470,7 @@ describe('chain history mappers', () => {
459470
});
460471
describe('mapTxOutModel', () => {
461472
test('map TxOutputModel with assets to TxOutput', () => {
462-
const result = mappers.mapTxOutModel(txOutModel, assets);
473+
const result = mappers.mapTxOutModel(txOutModel, { assets });
463474
expect(result).toEqual<TxOutput>({
464475
address: Cardano.PaymentAddress(address),
465476
datumHash: Hash32ByteBase16(hash32ByteBase16),
@@ -468,8 +479,21 @@ describe('chain history mappers', () => {
468479
value: { assets, coins: 20_000_000n }
469480
});
470481
});
482+
483+
test('map TxOutputModel with reference script to TxOutput', () => {
484+
const result = mappers.mapTxOutModel(txOutModel, { assets, script });
485+
expect(result).toEqual<TxOutput>({
486+
address: Cardano.PaymentAddress(address),
487+
datumHash: Hash32ByteBase16(hash32ByteBase16),
488+
index: 1,
489+
scriptReference: script,
490+
txId: Cardano.TransactionId(transactionHash),
491+
value: { assets, coins: 20_000_000n }
492+
});
493+
});
494+
471495
test('map TxOutputModel with no assets to TxOutput', () => {
472-
const result = mappers.mapTxOutModel(txOutModel);
496+
const result = mappers.mapTxOutModel(txOutModel, {});
473497
expect(result).toEqual<TxOutput>({
474498
address: Cardano.PaymentAddress(address),
475499
datumHash: Hash32ByteBase16(hash32ByteBase16),
@@ -479,7 +503,7 @@ describe('chain history mappers', () => {
479503
});
480504
});
481505
test('map TxOutputModel with nulls to TxOutput', () => {
482-
const result = mappers.mapTxOutModel({ ...txOutModel, datum: null });
506+
const result = mappers.mapTxOutModel({ ...txOutModel, datum: null }, {});
483507
expect(result).toEqual<TxOutput>({
484508
address: Cardano.PaymentAddress(address),
485509
index: 1,

packages/cardano-services/test/ChainHistory/fixtures/FixtureBuilder.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export enum TxWith {
1818
MultiAsset = 'multiAsset',
1919
Redeemer = 'redeemer',
2020
Withdrawal = 'withdrawal',
21-
CollateralOutput = 'collateralOutput'
21+
CollateralOutput = 'collateralOutput',
22+
ScriptReference = 'scriptReference'
2223
}
2324

2425
export type AddressesInBlockRange = {
@@ -122,19 +123,20 @@ export class ChainHistoryFixtureBuilder {
122123
if (options.with.includes(TxWith.MirCertificate)) query += Queries.latestTxHashesWithMirCerts;
123124
if (options.with.includes(TxWith.Withdrawal)) query += Queries.latestTxHashesWithWithdrawal;
124125
if (options.with.includes(TxWith.CollateralOutput)) query += Queries.latestTxHashesWithCollateralOutput;
126+
if (options.with.includes(TxWith.ScriptReference)) query += Queries.latestTxHashesWithScriptReference;
125127

126128
query += Queries.endLatestTxHashes;
127129
}
128130

129-
const result: QueryResult<{ hash: Buffer }> = await this.#db.query(query, [desiredQty]);
131+
const result: QueryResult<{ tx_hash: Buffer }> = await this.#db.query(query, [desiredQty]);
130132

131133
const resultsQty = result.rows.length;
132134
if (result.rows.length === 0) {
133135
throw new Error('No transactions found');
134136
} else if (resultsQty < desiredQty) {
135137
this.#logger.warn(`${desiredQty} transactions desired, only ${resultsQty} results found`);
136138
}
137-
return result.rows.map(({ hash }) => bufferToHexString(hash) as unknown as Cardano.TransactionId);
139+
return result.rows.map(({ tx_hash }) => bufferToHexString(tx_hash) as unknown as Cardano.TransactionId);
138140
}
139141

140142
public async getMultiAssetTxOutIds(desiredQty: number) {

packages/cardano-services/test/ChainHistory/fixtures/queries.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ export const latestBlockHashes = `
1212
LIMIT $1`;
1313

1414
export const latestTxHashes = `
15-
SELECT hash
15+
SELECT tx.hash as tx_hash
1616
FROM tx
1717
ORDER BY id DESC
1818
LIMIT $1`;
1919

2020
export const beginLatestTxHashes = `
21-
SELECT hash FROM tx
21+
SELECT tx.hash as tx_hash FROM tx
2222
JOIN tx_out ON tx_out.tx_id = tx.id`;
2323

2424
export const latestTxHashesWithMultiAsset = `
@@ -60,6 +60,10 @@ export const latestTxHashesWithWithdrawal = `
6060
export const latestTxHashesWithCollateralOutput = `
6161
JOIN collateral_tx_out ON collateral_tx_out.tx_id = tx.id`;
6262

63+
export const latestTxHashesWithScriptReference = `
64+
JOIN script ON script.tx_id = tx.id
65+
WHERE tx_out.reference_script_id IS NOT NULL`;
66+
6367
export const endLatestTxHashes = `
6468
GROUP BY tx.id
6569
ORDER BY tx.id DESC

packages/core/src/Serialization/Common/Datum.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const DATUM_ARRAY_SIZE = 2;
1111
*
1212
* @param datum The datum to be checked for.
1313
*/
14-
const isDatumHash = (datum: unknown): datum is Cardano.DatumHash => datum !== null && typeof datum === 'string';
14+
export const isDatumHash = (datum: unknown): datum is Cardano.DatumHash => datum !== null && typeof datum === 'string';
1515

1616
/** Represents different ways of associating a Datum with a UTxO in a transaction. */
1717
export enum DatumKind {

packages/core/src/Serialization/PlutusData/PlutusData.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as Cardano from '../../Cardano';
2+
import * as Crypto from '@cardano-sdk/crypto';
23
import { CborReader, CborReaderState, CborTag, CborWriter } from '../CBOR';
34
import { ConstrPlutusData } from './ConstrPlutusData';
45
import { HexBlob } from '@cardano-sdk/util';
@@ -11,6 +12,7 @@ import { bytesToHex } from '../../util/misc';
1112
const MAX_WORD64 = 18_446_744_073_709_551_615n;
1213
const INDEFINITE_BYTE_STRING = new Uint8Array([95]);
1314
const MAX_BYTE_STRING_CHUNK_SIZE = 64;
15+
const HASH_LENGTH_IN_BYTES = 32;
1416

1517
/**
1618
* A type corresponding to the Plutus Core Data datatype.
@@ -212,6 +214,17 @@ export class PlutusData {
212214
}
213215
}
214216

217+
/**
218+
* Computes the plutus data hash.
219+
*
220+
* @returns the plutus data hash.
221+
*/
222+
hash(): Crypto.Hash32ByteBase16 {
223+
const hash = Crypto.blake2b(HASH_LENGTH_IN_BYTES).update(Buffer.from(this.toCbor(), 'hex')).digest();
224+
225+
return Crypto.Hash32ByteBase16(HexBlob.fromBytes(hash));
226+
}
227+
215228
/**
216229
* Creates a PlutusData object from the given Core PlutusData object.
217230
*

0 commit comments

Comments
 (0)