diff --git a/.changeset/eleven-walls-return.md b/.changeset/eleven-walls-return.md new file mode 100644 index 00000000..0f194a9e --- /dev/null +++ b/.changeset/eleven-walls-return.md @@ -0,0 +1,9 @@ +--- +"@blaze-cardano/emulator": minor +"@blaze-cardano/query": minor +"@blaze-cardano/core": minor +"@blaze-cardano/tx": minor +"@blaze-cardano/ogmios": patch +--- + +feat: Script deployment methods for tx and script-ref resolving provider query diff --git a/packages/blaze-core/src/util.ts b/packages/blaze-core/src/util.ts index 373477bf..e894c6a0 100644 --- a/packages/blaze-core/src/util.ts +++ b/packages/blaze-core/src/util.ts @@ -6,7 +6,6 @@ import type { PaymentAddress, Script, NetworkId, - Credential, Ed25519PrivateExtendedKeyHex, Ed25519PrivateNormalKeyHex, } from "./types"; @@ -18,6 +17,7 @@ import { AddressType, Hash32ByteBase16, Ed25519SignatureHex, + Credential, } from "./types"; import { sha256 } from "@noble/hashes/sha256"; import * as sha3 from "@noble/hashes/sha3"; @@ -269,6 +269,17 @@ export const addressFromCredentials = ( }); }; +const burnCred = Credential.fromCore({ + hash: Hash28ByteBase16( + // From https://cardano-tools.io/burn-address + "bbece14f554b0020fe2715d05801f4680ebd40d11a58f14740b9f2c5", + ), + type: CredentialType.ScriptHash, +}); + +export const getBurnAddress = (network: NetworkId) => + addressFromCredential(network, burnCred); + /** * Interface for objects that can be serialized to CBOR. */ diff --git a/packages/blaze-emulator/src/provider.ts b/packages/blaze-emulator/src/provider.ts index b6572edd..a0e66c1c 100644 --- a/packages/blaze-emulator/src/provider.ts +++ b/packages/blaze-emulator/src/provider.ts @@ -11,22 +11,24 @@ import { TransactionInput, PlutusData, TransactionOutput, + NetworkId, } from "@blaze-cardano/core"; import { TransactionUnspentOutput } from "@blaze-cardano/core"; -import type { Provider } from "@blaze-cardano/query"; +import { Provider } from "@blaze-cardano/query"; import type { Emulator } from "./emulator"; /** * The EmulatorProvider class implements the Provider interface. * It provides methods to interact with the Emulator. */ -export class EmulatorProvider implements Provider { +export class EmulatorProvider extends Provider { /** * The Emulator instance. */ private emulator: Emulator; constructor(emulator: Emulator) { + super(NetworkId.Testnet); this.emulator = emulator; } getParameters(): Promise { diff --git a/packages/blaze-emulator/test/Emulator.test.ts b/packages/blaze-emulator/test/Emulator.test.ts index 6760c51f..dd0fdc04 100644 --- a/packages/blaze-emulator/test/Emulator.test.ts +++ b/packages/blaze-emulator/test/Emulator.test.ts @@ -15,7 +15,6 @@ import { import { HotWallet } from "@blaze-cardano/wallet"; import { Emulator, EmulatorProvider } from "../src"; import { - DEPLOYMENT_ADDR, ONE_PLUTUS_DATA, VOID_PLUTUS_DATA, alwaysTrueScript, @@ -67,7 +66,8 @@ describe("Emulator", () => { }); test("Should be able to pay from one wallet to another", async () => { - const tx = await (await blaze.newTransaction()) + const tx = await blaze + .newTransaction() .payLovelace(wallet2.address, 2_000_000_000n) .complete(); const txHash = await signAndSubmit(tx, blaze); @@ -79,9 +79,8 @@ describe("Emulator", () => { }); test("Should be able to spend from a script", async () => { - const tx = await ( - await blaze.newTransaction() - ) + const tx = await blaze + .newTransaction() .lockAssets( addressFromCredential( NetworkId.Testnet, @@ -100,9 +99,8 @@ describe("Emulator", () => { const out = emulator.getOutput(inp); isDefined(out); isDefined(out.datum()); - const spendTx = await ( - await blaze.newTransaction() - ) + const spendTx = await blaze + .newTransaction() .addInput(new TransactionUnspentOutput(inp, out), VOID_PLUTUS_DATA) .lockAssets( addressFromCredential( @@ -125,25 +123,22 @@ describe("Emulator", () => { }); test("Should be able to spend from a script with a reference input", async () => { - const refTx = await (await blaze.newTransaction()) - .lockAssets( - DEPLOYMENT_ADDR, - makeValue(1_000_000_000n), - ONE_PLUTUS_DATA, - alwaysTrueScript, - ) + const refTx = await blaze + .newTransaction() + .deployScript(alwaysTrueScript) .complete(); const refTxHash = await signAndSubmit(refTx, blaze); emulator.awaitTransactionConfirmation(refTxHash); - const refIn = new TransactionInput(refTxHash, 0n); - const refUtxo = new TransactionUnspentOutput( - refIn, - emulator.getOutput(refIn)!, - ); + const refUtxo = await provider.resolveScriptRef(alwaysTrueScript); + isDefined(refUtxo); + // const refIn = new TransactionInput(refTxHash, 0n); + // const refUtxo = new TransactionUnspentOutput( + // refIn, + // emulator.getOutput(refIn)! + // ); - const tx = await ( - await blaze.newTransaction() - ) + const tx = await blaze + .newTransaction() .lockAssets( addressFromCredential( NetworkId.Testnet, @@ -164,7 +159,8 @@ describe("Emulator", () => { isDefined(out); - const spendTx = await (await blaze.newTransaction()) + const spendTx = await blaze + .newTransaction() .addInput(new TransactionUnspentOutput(inp, out), VOID_PLUTUS_DATA) .addReferenceInput(refUtxo) .complete(); @@ -184,7 +180,8 @@ describe("Emulator", () => { }), ); - const tx = await (await blaze.newTransaction()) + const tx = await blaze + .newTransaction() .lockAssets( addr, makeValue(1_000_000_000n), @@ -200,7 +197,8 @@ describe("Emulator", () => { isDefined(out); - const spendTx = await (await blaze.newTransaction()) + const spendTx = await blaze + .newTransaction() .addInput(new TransactionUnspentOutput(inp, out), VOID_PLUTUS_DATA) .complete(); const spendTxHash = await signAndSubmit(spendTx, blaze); @@ -212,9 +210,8 @@ describe("Emulator", () => { test("Should be able to mint from a policy", async () => { const policy = PolicyId(alwaysTrueScript.hash()); - const tx = await ( - await blaze.newTransaction() - ) + const tx = await blaze + .newTransaction() .addMint(policy, new Map([[AssetName(""), 1n]]), VOID_PLUTUS_DATA) .provideScript(alwaysTrueScript) .complete(); diff --git a/packages/blaze-emulator/test/util/index.ts b/packages/blaze-emulator/test/util/index.ts index 539c0f3c..521eeacb 100644 --- a/packages/blaze-emulator/test/util/index.ts +++ b/packages/blaze-emulator/test/util/index.ts @@ -7,16 +7,12 @@ import { Transaction, TransactionId, AddressType, - CredentialType, HexBlob, NetworkId, Address, Script, PlutusV2Script, PlutusData, - Credential, - Hash28ByteBase16, - addressFromCredential, } from "@blaze-cardano/core"; import type { Provider } from "@blaze-cardano/query"; import type { Blaze } from "@blaze-cardano/sdk"; @@ -28,17 +24,6 @@ export const generateSeedPhrase = () => generateMnemonic(wordlist); export const VOID_PLUTUS_DATA = PlutusData.fromCbor(HexBlob("00")); export const ONE_PLUTUS_DATA = PlutusData.fromCbor(HexBlob("01")); -// From https://cardano-tools.io/burn-address -export const LOCK_SCRIPT_HASH = - "bbece14f554b0020fe2715d05801f4680ebd40d11a58f14740b9f2c5"; -export const DEPLOYMENT_ADDR = addressFromCredential( - NetworkId.Testnet, - Credential.fromCore({ - hash: Hash28ByteBase16(LOCK_SCRIPT_HASH), - type: CredentialType.ScriptHash, - }), -); - export const SAMPLE_PLUTUS_DATA = PlutusData.fromCore( new Uint8Array([1, 2, 3]), ); diff --git a/packages/blaze-ogmios/src/unwrapped.ts b/packages/blaze-ogmios/src/unwrapped.ts index f5889157..b873f3bc 100644 --- a/packages/blaze-ogmios/src/unwrapped.ts +++ b/packages/blaze-ogmios/src/unwrapped.ts @@ -2,8 +2,8 @@ import WebSocket from "isomorphic-ws"; import type * as schema from "./schema"; export class Ogmios { + url: string; private ws: WebSocket; - private url: string; private requests: Record< string, { resolve: (value: any) => void; reject: (reason: any) => void } diff --git a/packages/blaze-query/src/blockfrost.ts b/packages/blaze-query/src/blockfrost.ts index 2991ea3f..64b56302 100644 --- a/packages/blaze-query/src/blockfrost.ts +++ b/packages/blaze-query/src/blockfrost.ts @@ -19,6 +19,7 @@ import { hardCodedProtocolParams, Hash28ByteBase16, HexBlob, + NetworkId, PlutusData, PlutusV1Script, PlutusV2Script, @@ -31,9 +32,9 @@ import { Value, } from "@blaze-cardano/core"; import { PlutusLanguageVersion } from "@blaze-cardano/core"; -import { purposeToTag, type Provider } from "./types"; +import { purposeToTag, Provider } from "./provider"; -export class Blockfrost implements Provider { +export class Blockfrost extends Provider { url: string; private projectId: string; @@ -48,6 +49,7 @@ export class Blockfrost implements Provider { | "cardano-sanchonet"; projectId: string; }) { + super(network == "cardano-mainnet" ? NetworkId.Mainnet : NetworkId.Testnet); this.url = `https://${network}.blockfrost.io/api/v0/`; this.projectId = projectId; } diff --git a/packages/blaze-query/src/index.ts b/packages/blaze-query/src/index.ts index 9228a239..6053cffd 100644 --- a/packages/blaze-query/src/index.ts +++ b/packages/blaze-query/src/index.ts @@ -1,4 +1,4 @@ export * from "./maestro"; export * from "./blockfrost"; export * from "./kupmios"; -export * from "./types"; +export * from "./provider"; diff --git a/packages/blaze-query/src/kupmios.ts b/packages/blaze-query/src/kupmios.ts index 0a23713b..2497a0e8 100644 --- a/packages/blaze-query/src/kupmios.ts +++ b/packages/blaze-query/src/kupmios.ts @@ -25,12 +25,13 @@ import { PlutusV1Script, PlutusV2Script, PlutusV3Script, + NetworkId, } from "@blaze-cardano/core"; -import { purposeToTag, type Provider } from "./types"; +import { purposeToTag, Provider } from "./provider"; import type { Unwrapped } from "@blaze-cardano/ogmios"; import type * as Schema from "@cardano-ogmios/schema"; -export class Kupmios implements Provider { +export class Kupmios extends Provider { kupoUrl: string; ogmios: Unwrapped.Ogmios; @@ -48,6 +49,9 @@ export class Kupmios implements Provider { * @param ogmiosUrl - URL of the Ogmios service. */ constructor(kupoUrl: string, ogmios: Unwrapped.Ogmios) { + super( + ogmios.url.includes("mainnet-v6") ? NetworkId.Mainnet : NetworkId.Testnet, + ); this.kupoUrl = kupoUrl; this.ogmios = ogmios; } diff --git a/packages/blaze-query/src/maestro.ts b/packages/blaze-query/src/maestro.ts index 7d047961..4c3f0e9c 100644 --- a/packages/blaze-query/src/maestro.ts +++ b/packages/blaze-query/src/maestro.ts @@ -6,7 +6,7 @@ import type { CostModels, Credential, } from "@blaze-cardano/core"; -import { RedeemerTag } from "@blaze-cardano/core"; +import { NetworkId, RedeemerTag } from "@blaze-cardano/core"; import { TransactionUnspentOutput, Address, @@ -21,9 +21,9 @@ import { Redeemers, ExUnits, } from "@blaze-cardano/core"; -import type { Provider } from "./types"; +import { Provider } from "./provider"; -export class Maestro implements Provider { +export class Maestro extends Provider { private url: string; private apiKey: string; @@ -34,6 +34,7 @@ export class Maestro implements Provider { network: "mainnet" | "preview" | "preprod"; apiKey: string; }) { + super(network == "mainnet" ? NetworkId.Mainnet : NetworkId.Testnet); this.url = `https://${network}.gomaestro-api.org/v1`; this.apiKey = apiKey; } diff --git a/packages/blaze-query/src/types.ts b/packages/blaze-query/src/provider.ts similarity index 72% rename from packages/blaze-query/src/types.ts rename to packages/blaze-query/src/provider.ts index e79f5843..d13cf5ca 100644 --- a/packages/blaze-query/src/types.ts +++ b/packages/blaze-query/src/provider.ts @@ -9,8 +9,12 @@ import { type Transaction, type ProtocolParameters, type Redeemers, + type NetworkId, + type Hash28ByteBase16, RedeemerPurpose, RedeemerTag, + Script, + getBurnAddress, } from "@blaze-cardano/core"; /** @@ -18,6 +22,12 @@ import { * This class provides an interface for interacting with the blockchain. */ export abstract class Provider { + network: NetworkId; + + constructor(network: NetworkId) { + this.network = network; + } + /** * Retrieves the parameters for a transaction. * @@ -107,6 +117,41 @@ export abstract class Provider { tx: Transaction, additionalUtxos: TransactionUnspentOutput[], ): Promise; + + /** + * Resolves the script deployment by finding a UTxO containing the script reference. + * + * @param {Script | Hash28ByteBase16} script - The script or its hash to resolve. + * @param {Address} [address] - The address to search for the script deployment. Defaults to a burn address. + * @returns {Promise} - The UTxO containing the script reference, or undefined if not found. + * + * @remarks + * This is a default implementation that works but may not be optimal. + * Subclasses of Provider should implement their own version for better performance. + * + * The method searches for a UTxO at the given address (or a burn address by default) + * that contains a script reference matching the provided script or script hash. + * + * @example + * ```typescript + * const scriptUtxo = await provider.resolveScriptRef(myScript); + * if (scriptUtxo) { + * console.log("Script found in UTxO:", scriptUtxo.input().toCore()); + * } else { + * console.log("Script not found"); + * } + * ``` + */ + async resolveScriptRef( + script: Script | Hash28ByteBase16, + address: Address = getBurnAddress(this.network), + ): Promise { + const utxos = await this.getUnspentOutputs(address); + if (script instanceof Script) { + script = script.hash(); + } + return utxos.find((utxo) => utxo.output().scriptRef()?.hash() === script); + } } /** diff --git a/packages/blaze-tx/src/tx.ts b/packages/blaze-tx/src/tx.ts index 75991f4c..41793477 100644 --- a/packages/blaze-tx/src/tx.ts +++ b/packages/blaze-tx/src/tx.ts @@ -57,6 +57,7 @@ import { blake2b_256, RedeemerTag, StakeRegistration, + getBurnAddress, } from "@blaze-cardano/core"; import * as value from "./value"; import { micahsSelector, type SelectionResult } from "./coinSelection"; @@ -136,6 +137,7 @@ export class TxBuilder { inputs: TransactionUnspentOutput[], dearth: Value, ) => SelectionResult = micahsSelector; + private _burnAddress?: Address; /** * Constructs a new instance of the TxBuilder class. @@ -151,6 +153,13 @@ export class TxBuilder { ); } + get burnAddress(): Address { + if (!this._burnAddress) { + this._burnAddress = getBurnAddress(this.networkId!); + } + return this._burnAddress; + } + private insertSorted(arr: T[], el: T) { const index = arr.findIndex((x) => x.localeCompare(el) > 0); if (index == -1) { @@ -726,6 +735,30 @@ export class TxBuilder { ); } + /** + * Deploys a script by creating a new UTxO with the script as its reference. + * + * @param {Script} script - The script to be deployed. + * @param {Address} [address] - The address to lock the script to. Defaults to a burn address where the UTxO will be unspendable. + * @returns {TxBuilder} The same transaction builder. + * + * + * @example + * ```typescript + * const myScript = Script.newPlutusV2Script(new PlutusV2Script("...")); + * txBuilder.deployScript(myScript); + * // or + * txBuilder.deployScript(myScript, someAddress); + * ``` + */ + deployScript(script: Script, address: Address = this.burnAddress): TxBuilder { + const out = new TransactionOutput(address, new Value(0n)); + out.setScriptRef(script); + out.amount().setCoin(this.calculateMinAda(out)); + this.addOutput(out); + return this; + } + /** * Adds a Plutus datum to the transaction. This datum is not directly associated with any particular output but may be used * by scripts during transaction validation. This method is useful for including additional information that scripts may diff --git a/sites/docs/next-env.d.ts b/sites/docs/next-env.d.ts index 4f11a03d..a4a7b3f5 100644 --- a/sites/docs/next-env.d.ts +++ b/sites/docs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.