Skip to content

Commit

Permalink
feat: Blockfrost provider returns UTxOs with datums and script refs (#91
Browse files Browse the repository at this point in the history
)
  • Loading branch information
EzePze authored Jul 24, 2024
1 parent b847e16 commit f0854dc
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-panthers-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blaze-cardano/query": patch
---

feat: Blockfrost provider returns UTxOs with datums and script refs
18 changes: 18 additions & 0 deletions examples/blockfrost/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ const txIns = [
"74baf638a18a6ab54798cac310112af61cb1f1d6eacb8c893a10b6877cf71a8d",
1,
),
new TransactionInput(
"036ed48c89169a9c4e475e08a6d22ea1e09ba8a6a6cfbabfa4f1deefd269652c",
0,
),
new TransactionInput(
"ad54aa407df81404ab74343a4fca56e51c4ab488c54f12c4bc43e8b093c976a5",
1,
),
];

const resolvedOutputs = await provider.resolveUnspentOutputs(txIns);
Expand All @@ -110,6 +118,16 @@ for (const utxo of resolvedOutputs) {

console.log(`Amount of ${assetName}: ${amount}`);
}

if (utxo.output().datum()) {
console.log("Datum:");
console.log(utxo.output().datum().toCore());
}

if (utxo.output().scriptRef()) {
console.log("Reference Script:");
console.log(utxo.output().scriptRef().hash());
}
}

const datumHash =
Expand Down
143 changes: 110 additions & 33 deletions packages/blaze-query/src/blockfrost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@ import type {
AssetId as AssetIdType,
CostModels,
Credential,
DatumHash,
ProtocolParameters,
Redeemer,
ScriptHash,
TokenMap,
Transaction,
} from "@blaze-cardano/core";
import {
Address,
AddressType,
AssetId,
Datum,
DatumHash,
ExUnits,
fromHex,
Hash28ByteBase16,
HexBlob,
PlutusData,
PlutusV1Script,
PlutusV2Script,
Redeemers,
RedeemerTag,
Script,
TransactionId,
TransactionInput,
TransactionOutput,
Expand Down Expand Up @@ -147,7 +153,7 @@ export class Blockfrost implements Provider {
paymentPart: address.toCore(),
}).toBech32();

const buildTxUnspentOutput = buildTransactionUnspentOutput(
const buildTxUnspentOutput = this.buildTransactionUnspentOutput(
Address.fromBech32(bech32),
);

Expand All @@ -173,7 +179,7 @@ export class Blockfrost implements Provider {
}

for (const blockfrostUTxO of response) {
results.add(buildTxUnspentOutput(blockfrostUTxO));
results.add(await buildTxUnspentOutput(blockfrostUTxO));
}

if (response.length < maxPageCount) {
Expand Down Expand Up @@ -212,7 +218,7 @@ export class Blockfrost implements Provider {
paymentPart: address.toCore(),
}).toBech32();

const buildTxUnspentOutput = buildTransactionUnspentOutput(
const buildTxUnspentOutput = this.buildTransactionUnspentOutput(
Address.fromBech32(bech32),
);

Expand Down Expand Up @@ -242,7 +248,7 @@ export class Blockfrost implements Provider {
}

for (const blockfrostUTxO of response) {
results.add(buildTxUnspentOutput(blockfrostUTxO));
results.add(await buildTxUnspentOutput(blockfrostUTxO));
}

if (response.length < maxPageCount) {
Expand Down Expand Up @@ -349,11 +355,11 @@ export class Blockfrost implements Provider {
// Blockfrost API does not return tx hash, so it must be manually set
blockfrostUTxO.tx_hash = txIn.transactionId();

const buildTxUnspentOutput = buildTransactionUnspentOutput(
const buildTxUnspentOutput = this.buildTransactionUnspentOutput(
Address.fromBech32(blockfrostUTxO.address),
);

results.add(buildTxUnspentOutput(blockfrostUTxO));
results.add(await buildTxUnspentOutput(blockfrostUTxO));
}
}

Expand Down Expand Up @@ -584,6 +590,103 @@ export class Blockfrost implements Provider {
Array.from(evaledRedeemers).map((x) => x.toCore()),
);
}

private async getScriptRef(scriptHash: ScriptHash): Promise<Script> {
const typeQuery = `/scripts/${scriptHash}`;
const typeJson = await fetch(`${this.url}${typeQuery}`, {
headers: this.headers(),
}).then((resp) => resp.json());

if (!typeJson) {
throw new Error("getScriptRef: Could not parse response json");
}

const typeResponse = typeJson as BlockfrostResponse<{
type: "timelock" | "plutusV1" | "plutusV2";
}>;

if ("message" in typeResponse) {
throw new Error(
`getScriptRef: Blockfrost threw "${typeResponse.message}"`,
);
}

const type = typeResponse.type;
if (type == "timelock") {
throw new Error("getScriptRef: Native scripts are not yet supported.");
}

const cborQuery = `/scripts/${scriptHash}/cbor`;
const cborJson = await fetch(`${this.url}${cborQuery}`, {
headers: this.headers(),
}).then((resp) => resp.json());

if (!cborJson) {
throw new Error("getScriptRef: Could not parse response json");
}

const cborResponse = cborJson as BlockfrostResponse<{
cbor: string;
}>;

if ("message" in cborResponse) {
throw new Error(
`getScriptRef: Blockfrost threw "${cborResponse.message}"`,
);
}

const cbor = HexBlob(cborResponse.cbor);

switch (type) {
case "plutusV1":
return Script.newPlutusV1Script(new PlutusV1Script(cbor));
case "plutusV2":
return Script.newPlutusV2Script(new PlutusV2Script(cbor));
}
}

// Partially applies address in order to avoid sending it
// as argument repeatedly when building TransactionUnspentOutput
private buildTransactionUnspentOutput(
address: Address,
): (blockfrostUTxO: BlockfrostUTxO) => Promise<TransactionUnspentOutput> {
return async (blockfrostUTxO) => {
const txIn = new TransactionInput(
TransactionId(blockfrostUTxO.tx_hash),
BigInt(blockfrostUTxO.output_index),
);
// No tx output CBOR available from Blockfrost,
// so TransactionOutput must be manually constructed.
const tokenMap: TokenMap = new Map<AssetId, bigint>();
let lovelace = 0n;
for (const { unit, quantity } of blockfrostUTxO.amount) {
if (unit === "lovelace") {
lovelace = BigInt(quantity);
} else {
tokenMap.set(unit as AssetId, BigInt(quantity));
}
}
const txOut = new TransactionOutput(
address,
new Value(lovelace, tokenMap),
);
const datum = blockfrostUTxO.inline_datum
? Datum.newInlineData(
PlutusData.fromCbor(HexBlob(blockfrostUTxO.inline_datum)),
)
: blockfrostUTxO.data_hash
? Datum.newDataHash(DatumHash(blockfrostUTxO.data_hash))
: undefined;
if (datum) txOut.setDatum(datum);
if (blockfrostUTxO.reference_script_hash)
txOut.setScriptRef(
await this.getScriptRef(
Hash28ByteBase16(blockfrostUTxO.reference_script_hash),
),
);
return new TransactionUnspentOutput(txIn, txOut);
};
}
}

// builds proper type from string result from Blockfrost API
Expand All @@ -606,32 +709,6 @@ function purposeFromTag(tag: string): RedeemerTag {
}
}

// Partially applies address in order to avoid sending it
// as argument repeatedly when building TransactionUnspentOutput
function buildTransactionUnspentOutput(
address: Address,
): (blockfrostUTxO: BlockfrostUTxO) => TransactionUnspentOutput {
return (blockfrostUTxO) => {
const txIn = new TransactionInput(
TransactionId(blockfrostUTxO.tx_hash),
BigInt(blockfrostUTxO.output_index),
);
// No tx output CBOR available from Blockfrost,
// so TransactionOutput must be manually constructed.
const tokenMap: TokenMap = new Map<AssetId, bigint>();
let lovelace = 0n;
for (const { unit, quantity } of blockfrostUTxO.amount) {
if (unit === "lovelace") {
lovelace = BigInt(quantity);
} else {
tokenMap.set(unit as AssetId, BigInt(quantity));
}
}
const txOut = new TransactionOutput(address, new Value(lovelace, tokenMap));
return new TransactionUnspentOutput(txIn, txOut);
};
}

type BlockfrostLanguageVersions = "PlutusV1" | "PlutusV2" | "PlutusV3";
export const fromBlockfrostLanguageVersion = (
x: BlockfrostLanguageVersions,
Expand Down

0 comments on commit f0854dc

Please sign in to comment.