|
| 1 | +/** |
| 2 | + * Devnet test: a new inline datum is written with the plutusData transaction |
| 3 | + * options, and the node keeps it as written. |
| 4 | + * |
| 5 | + * The builder writes a transaction with an inline map datum. Each case |
| 6 | + * rebuilds that transaction with a new InlineDatum, encodes it with |
| 7 | + * Transaction.toCBORHex under one option set, signs it over the raw body with |
| 8 | + * Transaction.addVKeyWitnessesBytes, and submits the bytes. The ledger returns |
| 9 | + * the datum, which must be the bytes Data.toCBORHex writes under the case's |
| 10 | + * plutusData options. |
| 11 | + */ |
| 12 | + |
| 13 | +import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" |
| 14 | +import * as Cluster from "@evolution-sdk/devnet/Cluster" |
| 15 | +import * as Config from "@evolution-sdk/devnet/Config" |
| 16 | +import * as Genesis from "@evolution-sdk/devnet/Genesis" |
| 17 | +import { Cardano, Client, preprod } from "@evolution-sdk/evolution" |
| 18 | +import * as Address from "@evolution-sdk/evolution/Address" |
| 19 | +import * as Bytes from "@evolution-sdk/evolution/Bytes" |
| 20 | +import * as CBOR from "@evolution-sdk/evolution/CBOR" |
| 21 | +import * as Data from "@evolution-sdk/evolution/Data" |
| 22 | +import * as InlineDatum from "@evolution-sdk/evolution/InlineDatum" |
| 23 | +import * as Transaction from "@evolution-sdk/evolution/Transaction" |
| 24 | +import * as TransactionBody from "@evolution-sdk/evolution/TransactionBody" |
| 25 | +import * as TransactionHash from "@evolution-sdk/evolution/TransactionHash" |
| 26 | +import * as TransactionWitnessSet from "@evolution-sdk/evolution/TransactionWitnessSet" |
| 27 | +import * as TxOut from "@evolution-sdk/evolution/TxOut" |
| 28 | + |
| 29 | +describe("Inline datum options (Devnet Submit)", () => { |
| 30 | + let devnetCluster: Cluster.Cluster | undefined |
| 31 | + let genesisUtxos: ReadonlyArray<Cardano.UTxO.UTxO> = [] |
| 32 | + |
| 33 | + const TEST_MNEMONIC = |
| 34 | + "test test test test test test test test test test test test test test test test test test test test test test test sauce" |
| 35 | + |
| 36 | + // {1: [2]}: each option set below writes it differently, or with a |
| 37 | + // different ledger layout around it |
| 38 | + const datum = Data.map([[Data.int(1n), Data.list([Data.int(2n)])]]) |
| 39 | + |
| 40 | + const cases: ReadonlyArray<{ name: string; options: CBOR.TxCodecOptions; datumHex: string }> = [ |
| 41 | + { name: "CBOR.TX_DEFAULT_OPTIONS", options: CBOR.TX_DEFAULT_OPTIONS, datumHex: "a1019f02ff" }, |
| 42 | + { name: "CBOR.TX_CANONICAL_OPTIONS", options: CBOR.TX_CANONICAL_OPTIONS, datumHex: "a1018102" }, |
| 43 | + { |
| 44 | + name: "plutusData CML_DATA_DEFAULT_OPTIONS", |
| 45 | + options: { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFAULT_OPTIONS }, |
| 46 | + datumHex: "bf019f02ffff" |
| 47 | + }, |
| 48 | + { |
| 49 | + name: "plutusData CML_DATA_DEFINITE_OPTIONS", |
| 50 | + options: { ledger: CBOR.CML_DEFAULT_OPTIONS, plutusData: CBOR.CML_DATA_DEFINITE_OPTIONS }, |
| 51 | + datumHex: "a1018102" |
| 52 | + } |
| 53 | + ] |
| 54 | + |
| 55 | + const ogmiosUrl = () => `http://localhost:${devnetCluster!.ports.ogmios}` |
| 56 | + |
| 57 | + const createTestClient = () => { |
| 58 | + if (!devnetCluster) throw new Error("Cluster not initialized") |
| 59 | + return Client.make(Cluster.getChain(devnetCluster)) |
| 60 | + .withKupmios({ kupoUrl: `http://localhost:${devnetCluster.ports.kupo}`, ogmiosUrl: ogmiosUrl() }) |
| 61 | + .withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" }) |
| 62 | + } |
| 63 | + |
| 64 | + const ogmios = async (method: string, params: unknown) => { |
| 65 | + const response = await fetch(ogmiosUrl(), { |
| 66 | + method: "POST", |
| 67 | + headers: { "Content-Type": "application/json" }, |
| 68 | + body: JSON.stringify({ jsonrpc: "2.0", method, params, id: null }) |
| 69 | + }) |
| 70 | + return (await response.json()) as { result?: any; error?: unknown } |
| 71 | + } |
| 72 | + |
| 73 | + beforeAll(async () => { |
| 74 | + const tempClient = Client.make(preprod).withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" }) |
| 75 | + const testAddressHex = Address.toHex(await tempClient.address()) |
| 76 | + |
| 77 | + const genesisConfig: Config.ShelleyGenesis = { |
| 78 | + ...Config.DEFAULT_SHELLEY_GENESIS, |
| 79 | + slotLength: 0.02, |
| 80 | + epochLength: 50, |
| 81 | + activeSlotsCoeff: 1.0, |
| 82 | + initialFunds: { [testAddressHex]: 500_000_000_000 } |
| 83 | + } |
| 84 | + |
| 85 | + genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig) |
| 86 | + |
| 87 | + devnetCluster = await Cluster.make({ |
| 88 | + clusterName: "inline-datum-options-test", |
| 89 | + shelleyGenesis: genesisConfig, |
| 90 | + kupo: { enabled: true, logLevel: "Info" }, |
| 91 | + ogmios: { enabled: true, logLevel: "info" } |
| 92 | + }) |
| 93 | + |
| 94 | + await Cluster.start(devnetCluster) |
| 95 | + await new Promise((resolve) => setTimeout(resolve, 3_000)) |
| 96 | + }, 180_000) |
| 97 | + |
| 98 | + afterAll(async () => { |
| 99 | + if (devnetCluster) { |
| 100 | + await Cluster.stop(devnetCluster) |
| 101 | + await Cluster.remove(devnetCluster) |
| 102 | + } |
| 103 | + }, 60_000) |
| 104 | + |
| 105 | + cases.forEach(({ datumHex, name, options }, caseIndex) => { |
| 106 | + it(`writes and keeps an inline map datum under ${name}`, { timeout: 120_000 }, async () => { |
| 107 | + const client = createTestClient() |
| 108 | + const myAddress = await client.address() |
| 109 | + expect(Data.toCBORHex(datum, options.plutusData)).toBe(datumHex) |
| 110 | + |
| 111 | + const signBuilder = await client |
| 112 | + .newTx() |
| 113 | + .payToAddress({ |
| 114 | + address: myAddress, |
| 115 | + assets: Cardano.Assets.fromLovelace(5_000_000n), |
| 116 | + datum: new InlineDatum.InlineDatum({ data: datum }) |
| 117 | + }) |
| 118 | + .build(caseIndex === 0 ? { availableUtxos: [...genesisUtxos] } : undefined) |
| 119 | + const built = await signBuilder.toTransaction() |
| 120 | + |
| 121 | + // A new InlineDatum in each output that holds one, and a fee that |
| 122 | + // covers the few extra bytes, paid from the largest output |
| 123 | + const feeBump = 10_000n |
| 124 | + const changeIndex = built.body.outputs.reduce( |
| 125 | + (best, output, i) => |
| 126 | + Cardano.Assets.lovelaceOf(output.assets) > Cardano.Assets.lovelaceOf(built.body.outputs[best]!.assets) |
| 127 | + ? i |
| 128 | + : best, |
| 129 | + 0 |
| 130 | + ) |
| 131 | + const outputs = built.body.outputs.map( |
| 132 | + (output, i) => |
| 133 | + new TxOut.TransactionOutput({ |
| 134 | + ...output, |
| 135 | + assets: |
| 136 | + i === changeIndex ? Cardano.Assets.subtractLovelace(output.assets, feeBump) : output.assets, |
| 137 | + datumOption: |
| 138 | + output.datumOption?._tag === "InlineDatum" |
| 139 | + ? new InlineDatum.InlineDatum({ data: output.datumOption.data }) |
| 140 | + : output.datumOption |
| 141 | + }) |
| 142 | + ) |
| 143 | + const datumIndex = outputs.findIndex((output) => output.datumOption?._tag === "InlineDatum") |
| 144 | + expect(datumIndex).toBeGreaterThanOrEqual(0) |
| 145 | + const tx = new Transaction.Transaction({ |
| 146 | + ...built, |
| 147 | + body: new TransactionBody.TransactionBody({ ...built.body, outputs, fee: built.body.fee + feeBump }) |
| 148 | + }) |
| 149 | + |
| 150 | + const txHex = Transaction.toCBORHex(tx, options) |
| 151 | + expect(txHex).toContain(`d818${(0x40 + datumHex.length / 2).toString(16)}${datumHex}`) |
| 152 | + const txBytes = Bytes.fromHex(txHex) |
| 153 | + const bodyBytes = Transaction.extractBodyBytes(txBytes) |
| 154 | + const expectedTxId = TransactionHash.toHex(TransactionBody.toHashFromBytes(bodyBytes)) |
| 155 | + |
| 156 | + const walletWitnessSet = await client.signTx(txHex, { |
| 157 | + utxos: caseIndex === 0 ? [...genesisUtxos] : await client.getUtxos(myAddress) |
| 158 | + }) |
| 159 | + const signedBytes = Transaction.addVKeyWitnessesBytes(txBytes, TransactionWitnessSet.toCBORBytes(walletWitnessSet)) |
| 160 | + expect(Bytes.toHex(Transaction.extractBodyBytes(signedBytes))).toBe(Bytes.toHex(bodyBytes)) |
| 161 | + |
| 162 | + const submitted = await ogmios("submitTransaction", { transaction: { cbor: Bytes.toHex(signedBytes) } }) |
| 163 | + expect(submitted.error).toBeUndefined() |
| 164 | + expect(submitted.result.transaction.id).toBe(expectedTxId) |
| 165 | + expect(await client.awaitTx(TransactionHash.fromHex(expectedTxId), 1000)).toBe(true) |
| 166 | + |
| 167 | + const utxos = await ogmios("queryLedgerState/utxo", { |
| 168 | + outputReferences: [{ transaction: { id: expectedTxId }, index: datumIndex }] |
| 169 | + }) |
| 170 | + expect((utxos.result as Array<{ datum?: string }>).map((utxo) => utxo.datum)).toEqual([datumHex]) |
| 171 | + await new Promise((resolve) => setTimeout(resolve, 2_000)) |
| 172 | + }) |
| 173 | + }) |
| 174 | +}) |
0 commit comments