Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/cad 5432 add initial plutus script support js sdk #1299

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import {
PoolRegisterCertModel,
PoolRetireCertModel,
RedeemerModel,
ScriptModel,
StakeCertModel,
TransactionDataMap,
TxIdModel,
TxInput,
TxInputModel,
TxOutMultiAssetModel,
TxOutScriptMap,
TxOutTokenMap,
TxOutput,
TxOutputModel,
Expand All @@ -28,6 +30,7 @@ import { Pool, QueryResult } from 'pg';
import { Range, hexStringToBuffer } from '@cardano-sdk/util';
import {
mapCertificate,
mapPlutusScript,
mapRedeemer,
mapTxId,
mapTxInModel,
Expand Down Expand Up @@ -68,6 +71,28 @@ export class ChainHistoryBuilder {
return mapTxOutTokenMap(result.rows);
}

public async queryReferenceScriptsByTxOut(txOutModel: TxOutputModel[]): Promise<TxOutScriptMap> {
const txScriptMap: TxOutScriptMap = new Map();

for (const model of txOutModel) {
if (model.reference_script_id) {
const result: QueryResult<ScriptModel> = await this.#db.query({
name: 'tx_reference_scripts_by_tx_out_ids',
text: Queries.findReferenceScriptsById,
values: [[model.reference_script_id]]
});

if (result.rows.length === 0) continue;
if (result.rows[0].type === 'timelock') continue; // Shouldn't happen.

// There can only be one refScript per output.
txScriptMap.set(model.id, mapPlutusScript(result.rows[0]));
}
}

return txScriptMap;
}

public async queryTransactionOutputsByIds(ids: string[], collateral = false): Promise<TxOutput[]> {
this.#logger.debug(`About to find outputs (collateral: ${collateral}) for transactions with ids:`, ids);
const result: QueryResult<TxOutputModel> = await this.#db.query({
Expand All @@ -79,7 +104,14 @@ export class ChainHistoryBuilder {

const txOutIds = result.rows.flatMap((txOut) => BigInt(txOut.id));
const multiAssets = await this.queryMultiAssetsByTxOut(txOutIds);
return result.rows.map((txOut) => mapTxOutModel(txOut, multiAssets.get(txOut.id)));
const referenceScripts = await this.queryReferenceScriptsByTxOut(result.rows);

return result.rows.map((txOut) =>
mapTxOutModel(txOut, {
assets: multiAssets.get(txOut.id),
script: referenceScripts.get(txOut.id)
})
);
}

public async queryTxMintByIds(ids: string[]): Promise<TxTokenMap> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as Crypto from '@cardano-sdk/crypto';
import { BigIntMath } from '@cardano-sdk/util';
import { BigIntMath, HexBlob } from '@cardano-sdk/util';
import {
BlockModel,
BlockOutputModel,
CertificateModel,
MultiAssetModel,
RedeemerModel,
ScriptModel,
TipModel,
TxIdModel,
TxInput,
Expand Down Expand Up @@ -84,14 +85,18 @@ export const mapTxOut = (txOut: TxOutput): Cardano.TxOut => ({
value: txOut.value
});

export const mapTxOutModel = (txOutModel: TxOutputModel, assets?: Cardano.TokenMap): TxOutput => ({
export const mapTxOutModel = (
txOutModel: TxOutputModel,
props: { assets?: Cardano.TokenMap; script?: Cardano.Script }
): TxOutput => ({
address: txOutModel.address as unknown as Cardano.PaymentAddress,
// Inline datums are missing, but for now it's ok on ChainHistoryProvider
datumHash: txOutModel.datum ? (txOutModel.datum.toString('hex') as unknown as Hash32ByteBase16) : undefined,
index: txOutModel.index,
scriptReference: props.script,
txId: txOutModel.tx_id.toString('hex') as unknown as Cardano.TransactionId,
value: {
assets: assets && assets.size > 0 ? assets : undefined,
assets: props.assets && props.assets.size > 0 ? props.assets : undefined,
coins: BigInt(txOutModel.coin_value)
}
});
Expand All @@ -101,6 +106,21 @@ export const mapWithdrawal = (withdrawalModel: WithdrawalModel): Cardano.Withdra
stakeAddress: withdrawalModel.stake_address as unknown as Cardano.RewardAccount
});

export const mapPlutusScript = (scriptModel: ScriptModel): Cardano.Script => {
const cbor = scriptModel.bytes.toString('hex') as HexBlob;

return {
__type: Cardano.ScriptType.Plutus,
bytes: cbor,
version:
scriptModel.type === 'plutusV1'
? Cardano.PlutusLanguageVersion.V1
: scriptModel.type === 'plutusV2'
? Cardano.PlutusLanguageVersion.V2
: Cardano.PlutusLanguageVersion.V3
};
};

// TODO: unfortunately this is not nullable and not implemented.
// Remove this and select the actual redeemer data from `redeemer_data` table.
const stubRedeemerData = Buffer.from('not implemented');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const selectTxOutput = (collateral = false) => `
tx_out."index" AS "index",
tx_out.value AS coin_value,
tx_out.data_hash AS datum,
tx_out.reference_script_id as reference_script_id,
tx.hash AS tx_id
FROM ${collateral ? 'collateral_tx_out' : 'tx_out'} AS tx_out
JOIN tx ON tx_out.tx_id = tx.id`;
Expand Down Expand Up @@ -120,6 +121,14 @@ export const findMultiAssetByTxOut = `
WHERE tx_out.id = ANY($1)
ORDER BY ma_out.id ASC`;

export const findReferenceScriptsById = `
SELECT
script.type AS type,
script.bytes AS bytes,
script.serialised_size AS serialized_size
FROM script AS script
WHERE id = ANY($1)`;

export const findTxMintByIds = `
SELECT
mint.quantity AS quantity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Cardano } from '@cardano-sdk/core';
export type TransactionDataMap<T> = Map<Cardano.TransactionId, T>;
export type TxOutTokenMap = Map<string, Cardano.TokenMap>;
export type TxTokenMap = TransactionDataMap<Cardano.TokenMap>;
export type TxOutScriptMap = Map<string, Cardano.Script>;

export interface BlockModel {
block_no: number;
Expand Down Expand Up @@ -67,6 +68,7 @@ export interface TxOutputModel {
datum?: Buffer | null;
id: string;
index: number;
reference_script_id: number | null;
tx_id: Buffer;
}

Expand All @@ -87,6 +89,13 @@ export interface TxOutMultiAssetModel extends MultiAssetModel {
tx_out_id: string;
}

export interface ScriptModel {
type: 'timelock' | 'plutusV1' | 'plutusV2' | 'plutusV3';
bytes: Buffer;
hash: Buffer;
serialised_size: number;
}

export interface WithdrawalModel {
quantity: string;
tx_id: Buffer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,15 @@ describe('ChainHistoryHttpService', () => {
expect(tx.auxiliaryData).toBeDefined();
});

it('has script reference data', async () => {
const response = await provider.transactionsByHashes({
ids: await fixtureBuilder.getTxHashes(1, { with: [TxWith.ScriptReference] })
});
const tx: Cardano.HydratedTx = response[0];
expect(response.length).toEqual(1);
expect(tx.body.outputs.some((txOut) => !!txOut.scriptReference)).toBeTruthy();
});

it('has collateral inputs', async () => {
const response = await provider.transactionsByHashes({
ids: await fixtureBuilder.getTxHashes(1, { with: [TxWith.CollateralInput] })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from '../../../src/ChainHistory/DbSyncChainHistory/types';
import { Cardano } from '@cardano-sdk/core';
import { Hash28ByteBase16, Hash32ByteBase16 } from '@cardano-sdk/crypto';
import { HexBlob } from '@cardano-sdk/util';

const blockHash = '7a48b034645f51743550bbaf81f8a14771e58856e031eb63844738ca8ad72298';
const poolId = 'pool1zuevzm3xlrhmwjw87ec38mzs02tlkwec9wxpgafcaykmwg7efhh';
Expand Down Expand Up @@ -77,12 +78,22 @@ const txOutModel: TxOutputModel = {
datum: Buffer.from(hash32ByteBase16, 'hex'),
id: '1',
index: 1,
reference_script_id: 0,
tx_id: Buffer.from(transactionHash, 'hex')
};
const assets: Cardano.TokenMap = new Map([
[AssetId.TSLA, 500n],
[AssetId.PXL, 500n]
]);

const script: Cardano.PlutusScript = {
__type: Cardano.ScriptType.Plutus,
bytes: HexBlob(
'59079201000033232323232323232323232323232332232323232323232222232325335333006300800530070043333573466E1CD55CEA80124000466442466002006004646464646464646464646464646666AE68CDC39AAB9D500C480008CCCCCCCCCCCC88888888888848CCCCCCCCCCCC00403403002C02802402001C01801401000C008CD4060064D5D0A80619A80C00C9ABA1500B33501801A35742A014666AA038EB9406CD5D0A804999AA80E3AE501B35742A01066A0300466AE85401CCCD54070091D69ABA150063232323333573466E1CD55CEA801240004664424660020060046464646666AE68CDC39AAB9D5002480008CC8848CC00400C008CD40B9D69ABA15002302F357426AE8940088C98C80C8CD5CE01981901809AAB9E5001137540026AE854008C8C8C8CCCD5CD19B8735573AA004900011991091980080180119A8173AD35742A004605E6AE84D5D1280111931901919AB9C033032030135573CA00226EA8004D5D09ABA2500223263202E33573805E05C05826AAE7940044DD50009ABA1500533501875C6AE854010CCD540700808004D5D0A801999AA80E3AE200135742A00460446AE84D5D1280111931901519AB9C02B02A028135744A00226AE8940044D5D1280089ABA25001135744A00226AE8940044D5D1280089ABA25001135744A00226AE8940044D55CF280089BAA00135742A00460246AE84D5D1280111931900E19AB9C01D01C01A101B13263201B3357389201035054350001B135573CA00226EA80054049404448C88C008DD6000990009AA80A911999AAB9F0012500A233500930043574200460066AE880080548C8C8CCCD5CD19B8735573AA004900011991091980080180118061ABA150023005357426AE8940088C98C8054CD5CE00B00A80989AAB9E5001137540024646464646666AE68CDC39AAB9D5004480008CCCC888848CCCC00401401000C008C8C8C8CCCD5CD19B8735573AA0049000119910919800801801180A9ABA1500233500F014357426AE8940088C98C8068CD5CE00D80D00C09AAB9E5001137540026AE854010CCD54021D728039ABA150033232323333573466E1D4005200423212223002004357426AAE79400C8CCCD5CD19B875002480088C84888C004010DD71ABA135573CA00846666AE68CDC3A801A400042444006464C6403866AE700740700680640604D55CEA80089BAA00135742A00466A016EB8D5D09ABA2500223263201633573802E02C02826AE8940044D5D1280089AAB9E500113754002266AA002EB9D6889119118011BAB00132001355012223233335573E0044A010466A00E66442466002006004600C6AAE754008C014D55CF280118021ABA200301313574200222440042442446600200800624464646666AE68CDC3A800A40004642446004006600A6AE84D55CF280191999AB9A3370EA0049001109100091931900899AB9C01201100F00E135573AA00226EA80048C8C8CCCD5CD19B875001480188C848888C010014C01CD5D09AAB9E500323333573466E1D400920042321222230020053009357426AAE7940108CCCD5CD19B875003480088C848888C004014C01CD5D09AAB9E500523333573466E1D40112000232122223003005375C6AE84D55CF280311931900899AB9C01201100F00E00D00C135573AA00226EA80048C8C8CCCD5CD19B8735573AA004900011991091980080180118029ABA15002375A6AE84D5D1280111931900699AB9C00E00D00B135573CA00226EA80048C8CCCD5CD19B8735573AA002900011BAE357426AAE7940088C98C802CCD5CE00600580489BAA001232323232323333573466E1D4005200C21222222200323333573466E1D4009200A21222222200423333573466E1D400D2008233221222222233001009008375C6AE854014DD69ABA135744A00A46666AE68CDC3A8022400C4664424444444660040120106EB8D5D0A8039BAE357426AE89401C8CCCD5CD19B875005480108CC8848888888CC018024020C030D5D0A8049BAE357426AE8940248CCCD5CD19B875006480088C848888888C01C020C034D5D09AAB9E500B23333573466E1D401D2000232122222223005008300E357426AAE7940308C98C8050CD5CE00A80A00900880800780700680609AAB9D5004135573CA00626AAE7940084D55CF280089BAA0012323232323333573466E1D400520022333222122333001005004003375A6AE854010DD69ABA15003375A6AE84D5D1280191999AB9A3370EA0049000119091180100198041ABA135573CA00C464C6401A66AE7003803402C0284D55CEA80189ABA25001135573CA00226EA80048C8C8CCCD5CD19B875001480088C8488C00400CDD71ABA135573CA00646666AE68CDC3A8012400046424460040066EB8D5D09AAB9E500423263200A33573801601401000E26AAE7540044DD500089119191999AB9A3370EA00290021091100091999AB9A3370EA00490011190911180180218031ABA135573CA00846666AE68CDC3A801A400042444004464C6401666AE7003002C02402001C4D55CEA80089BAA0012323333573466E1D40052002212200223333573466E1D40092000212200123263200733573801000E00A00826AAE74DD5000891999AB9A3370E6AAE74DD5000A40004008464C6400866AE700140100092612001490103505431001123230010012233003300200200122212200201'
),
version: Cardano.PlutusLanguageVersion.V2
};

const multiAssetModel: MultiAssetModel = {
asset_name: Buffer.from(assetName, 'hex'),
fingerprint,
Expand Down Expand Up @@ -459,7 +470,7 @@ describe('chain history mappers', () => {
});
describe('mapTxOutModel', () => {
test('map TxOutputModel with assets to TxOutput', () => {
const result = mappers.mapTxOutModel(txOutModel, assets);
const result = mappers.mapTxOutModel(txOutModel, { assets });
expect(result).toEqual<TxOutput>({
address: Cardano.PaymentAddress(address),
datumHash: Hash32ByteBase16(hash32ByteBase16),
Expand All @@ -468,8 +479,21 @@ describe('chain history mappers', () => {
value: { assets, coins: 20_000_000n }
});
});

test('map TxOutputModel with reference script to TxOutput', () => {
const result = mappers.mapTxOutModel(txOutModel, { assets, script });
expect(result).toEqual<TxOutput>({
address: Cardano.PaymentAddress(address),
datumHash: Hash32ByteBase16(hash32ByteBase16),
index: 1,
scriptReference: script,
txId: Cardano.TransactionId(transactionHash),
value: { assets, coins: 20_000_000n }
});
});

test('map TxOutputModel with no assets to TxOutput', () => {
const result = mappers.mapTxOutModel(txOutModel);
const result = mappers.mapTxOutModel(txOutModel, {});
expect(result).toEqual<TxOutput>({
address: Cardano.PaymentAddress(address),
datumHash: Hash32ByteBase16(hash32ByteBase16),
Expand All @@ -479,7 +503,7 @@ describe('chain history mappers', () => {
});
});
test('map TxOutputModel with nulls to TxOutput', () => {
const result = mappers.mapTxOutModel({ ...txOutModel, datum: null });
const result = mappers.mapTxOutModel({ ...txOutModel, datum: null }, {});
expect(result).toEqual<TxOutput>({
address: Cardano.PaymentAddress(address),
index: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export enum TxWith {
MultiAsset = 'multiAsset',
Redeemer = 'redeemer',
Withdrawal = 'withdrawal',
CollateralOutput = 'collateralOutput'
CollateralOutput = 'collateralOutput',
ScriptReference = 'scriptReference'
}

export type AddressesInBlockRange = {
Expand Down Expand Up @@ -122,19 +123,20 @@ export class ChainHistoryFixtureBuilder {
if (options.with.includes(TxWith.MirCertificate)) query += Queries.latestTxHashesWithMirCerts;
if (options.with.includes(TxWith.Withdrawal)) query += Queries.latestTxHashesWithWithdrawal;
if (options.with.includes(TxWith.CollateralOutput)) query += Queries.latestTxHashesWithCollateralOutput;
if (options.with.includes(TxWith.ScriptReference)) query += Queries.latestTxHashesWithScriptReference;

query += Queries.endLatestTxHashes;
}

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

const resultsQty = result.rows.length;
if (result.rows.length === 0) {
throw new Error('No transactions found');
} else if (resultsQty < desiredQty) {
this.#logger.warn(`${desiredQty} transactions desired, only ${resultsQty} results found`);
}
return result.rows.map(({ hash }) => bufferToHexString(hash) as unknown as Cardano.TransactionId);
return result.rows.map(({ tx_hash }) => bufferToHexString(tx_hash) as unknown as Cardano.TransactionId);
}

public async getMultiAssetTxOutIds(desiredQty: number) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ export const latestBlockHashes = `
LIMIT $1`;

export const latestTxHashes = `
SELECT hash
SELECT tx.hash as tx_hash
FROM tx
ORDER BY id DESC
LIMIT $1`;

export const beginLatestTxHashes = `
SELECT hash FROM tx
SELECT tx.hash as tx_hash FROM tx
JOIN tx_out ON tx_out.tx_id = tx.id`;

export const latestTxHashesWithMultiAsset = `
Expand Down Expand Up @@ -60,6 +60,10 @@ export const latestTxHashesWithWithdrawal = `
export const latestTxHashesWithCollateralOutput = `
JOIN collateral_tx_out ON collateral_tx_out.tx_id = tx.id`;

export const latestTxHashesWithScriptReference = `
JOIN script ON script.tx_id = tx.id
WHERE tx_out.reference_script_id IS NOT NULL`;

export const endLatestTxHashes = `
GROUP BY tx.id
ORDER BY tx.id DESC
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/Serialization/Common/Datum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const DATUM_ARRAY_SIZE = 2;
*
* @param datum The datum to be checked for.
*/
const isDatumHash = (datum: unknown): datum is Cardano.DatumHash => datum !== null && typeof datum === 'string';
export const isDatumHash = (datum: unknown): datum is Cardano.DatumHash => datum !== null && typeof datum === 'string';

/** Represents different ways of associating a Datum with a UTxO in a transaction. */
export enum DatumKind {
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/Serialization/PlutusData/PlutusData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Cardano from '../../Cardano';
import * as Crypto from '@cardano-sdk/crypto';
import { CborReader, CborReaderState, CborTag, CborWriter } from '../CBOR';
import { ConstrPlutusData } from './ConstrPlutusData';
import { HexBlob } from '@cardano-sdk/util';
Expand All @@ -11,6 +12,7 @@ import { bytesToHex } from '../../util/misc';
const MAX_WORD64 = 18_446_744_073_709_551_615n;
const INDEFINITE_BYTE_STRING = new Uint8Array([95]);
const MAX_BYTE_STRING_CHUNK_SIZE = 64;
const HASH_LENGTH_IN_BYTES = 32;

/**
* A type corresponding to the Plutus Core Data datatype.
Expand Down Expand Up @@ -212,6 +214,17 @@ export class PlutusData {
}
}

/**
* Computes the plutus data hash.
*
* @returns the plutus data hash.
*/
hash(): Crypto.Hash32ByteBase16 {
const hash = Crypto.blake2b(HASH_LENGTH_IN_BYTES).update(Buffer.from(this.toCbor(), 'hex')).digest();

return Crypto.Hash32ByteBase16(HexBlob.fromBytes(hash));
}

/**
* Creates a PlutusData object from the given Core PlutusData object.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Cardano from '../../../Cardano';
import * as Crypto from '@cardano-sdk/crypto';
import { CborReader, CborWriter } from '../../CBOR';
import { ExUnits } from '../../Common';
import { HexBlob, InvalidArgumentError, InvalidStateError } from '@cardano-sdk/util';
Expand All @@ -7,6 +8,7 @@ import { RedeemerTag } from './RedeemerTag';
import { hexToBytes } from '../../../util/misc';

const REDEEMER_ARRAY_SIZE = 4;
const HASH_LENGTH_IN_BYTES = 32;

/**
* The Redeemer is an argument provided to a Plutus smart contract (script) when
Expand Down Expand Up @@ -243,4 +245,15 @@ export class Redeemer {
this.#exUnits = exUnits;
this.#originalBytes = undefined;
}

/**
* Computes the redeemer hash.
*
* @returns the redeemer hash.
*/
hash(): Crypto.Hash32ByteBase16 {
const hash = Crypto.blake2b(HASH_LENGTH_IN_BYTES).update(Buffer.from(this.toCbor(), 'hex')).digest();

return Crypto.Hash32ByteBase16(HexBlob.fromBytes(hash));
}
}
Loading
Loading