diff --git a/packages/hydra/package.json b/packages/hydra/package.json index cb27b2f3..5e364940 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@meshsdk/hydra", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "description": "Mesh Hydra package", "main": "./dist/index.cjs", "browser": "./dist/index.js", @@ -27,8 +27,8 @@ "test": "jest" }, "dependencies": { - "@meshsdk/common": "1.9.0-beta-39", - "@meshsdk/core-cst": "1.9.0-beta-39", + "@meshsdk/common": "1.9.0-beta-40", + "@meshsdk/core-cst": "1.9.0-beta-40", "axios": "^1.7.2" }, "devDependencies": { diff --git a/packages/mesh-common/package.json b/packages/mesh-common/package.json index e0ec8f18..a3afef30 100644 --- a/packages/mesh-common/package.json +++ b/packages/mesh-common/package.json @@ -1,6 +1,6 @@ { "name": "@meshsdk/common", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "description": "Contains constants, types and interfaces used across the SDK and different serialization libraries", "main": "./dist/index.cjs", "browser": "./dist/index.js", diff --git a/packages/mesh-common/src/types/transaction-builder/index.ts b/packages/mesh-common/src/types/transaction-builder/index.ts index a79b24eb..a80876e4 100644 --- a/packages/mesh-common/src/types/transaction-builder/index.ts +++ b/packages/mesh-common/src/types/transaction-builder/index.ts @@ -44,6 +44,8 @@ export type MeshTxBuilderBody = { network: Network | number[][]; expectedNumberKeyWitnesses: number; expectedByronAddressWitnesses: string[]; + totalCollateral?: Quantity; + collateralReturnAddress?: string; }; export const emptyTxBuilderBody = (): MeshTxBuilderBody => ({ diff --git a/packages/mesh-contract/package.json b/packages/mesh-contract/package.json index 90bdb7a6..73f96877 100644 --- a/packages/mesh-contract/package.json +++ b/packages/mesh-contract/package.json @@ -1,6 +1,6 @@ { "name": "@meshsdk/contract", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "description": "List of open-source smart contracts, complete with documentation, live demos, and end-to-end source code. https://meshjs.dev/smart-contracts", "main": "./dist/index.cjs", "browser": "./dist/index.js", @@ -34,8 +34,8 @@ "typescript": "^5.3.3" }, "dependencies": { - "@meshsdk/common": "1.9.0-beta-39", - "@meshsdk/core": "1.9.0-beta-39" + "@meshsdk/common": "1.9.0-beta-40", + "@meshsdk/core": "1.9.0-beta-40" }, "prettier": "@meshsdk/configs/prettier", "publishConfig": { diff --git a/packages/mesh-core-csl/package.json b/packages/mesh-core-csl/package.json index 127f0349..ae96396e 100644 --- a/packages/mesh-core-csl/package.json +++ b/packages/mesh-core-csl/package.json @@ -1,6 +1,6 @@ { "name": "@meshsdk/core-csl", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "description": "Types and utilities functions between Mesh and cardano-serialization-lib", "main": "./dist/index.cjs", "module": "./dist/index.js", @@ -31,7 +31,7 @@ }, "devDependencies": { "@meshsdk/configs": "*", - "@meshsdk/provider": "1.9.0-beta-39", + "@meshsdk/provider": "1.9.0-beta-40", "@types/json-bigint": "^1.0.4", "eslint": "^8.57.0", "ts-jest": "^29.1.4", @@ -39,7 +39,7 @@ "typescript": "^5.3.3" }, "dependencies": { - "@meshsdk/common": "1.9.0-beta-39", + "@meshsdk/common": "1.9.0-beta-40", "@sidan-lab/whisky-js-browser": "^1.0.1", "@sidan-lab/whisky-js-nodejs": "^1.0.1", "@types/base32-encoding": "^1.0.2", diff --git a/packages/mesh-core-cst/package.json b/packages/mesh-core-cst/package.json index 643d68e4..2b7aa33c 100644 --- a/packages/mesh-core-cst/package.json +++ b/packages/mesh-core-cst/package.json @@ -1,6 +1,6 @@ { "name": "@meshsdk/core-cst", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "description": "Types and utilities functions between Mesh and cardano-js-sdk", "main": "./dist/index.cjs", "browser": "./dist/index.js", @@ -44,7 +44,7 @@ "@harmoniclabs/plutus-data": "1.2.4", "@harmoniclabs/uplc": "1.2.4", "@harmoniclabs/pair": "^1.0.0", - "@meshsdk/common": "1.9.0-beta-39", + "@meshsdk/common": "1.9.0-beta-40", "@types/base32-encoding": "^1.0.2", "base32-encoding": "^1.0.0", "bech32": "^2.0.0", diff --git a/packages/mesh-core-cst/src/serializer/index.ts b/packages/mesh-core-cst/src/serializer/index.ts index 400cc34a..d760d4ce 100644 --- a/packages/mesh-core-cst/src/serializer/index.ts +++ b/packages/mesh-core-cst/src/serializer/index.ts @@ -105,11 +105,13 @@ import { TransactionInput, TransactionOutput, TransactionWitnessSet, + Value, VkeyWitness, } from "../types"; import { buildEd25519PrivateKeyFromSecretKey, fromBuilderToPlutusData, + mergeValue, toAddress, toCardanoAddress, toNativeScript, @@ -588,6 +590,9 @@ class CardanoSDKSerializerCore { certificates, withdrawals, votes, + totalCollateral, + collateralReturnAddress, + changeAddress, } = txBuilderBody; const uniqueRefInputs = this.removeBodyInputRefInputOverlap( @@ -603,6 +608,14 @@ class CardanoSDKSerializerCore { this.addAllWithdrawals(withdrawals); this.addAllVotes(votes); this.addAllCollateralInputs(collaterals); + if (totalCollateral) { + this.txBody.setTotalCollateral(BigInt(totalCollateral)); + this.addCollateralReturn( + totalCollateral, + collaterals, + collateralReturnAddress ?? changeAddress, + ); + } this.addAllReferenceInputs(uniqueRefInputs); this.removeInputRefInputOverlap(); this.setValidityInterval(validityRange); @@ -1234,6 +1247,27 @@ class CardanoSDKSerializerCore { this.txBody.setCollateral(collateralInputs); }; + private addCollateralReturn = ( + totalCollateral: string, + collaterals: PubKeyTxIn[], + collateralReturnAddress: string, + ) => { + let collateralReturnValue = Value.fromCore({ + coins: -BigInt(totalCollateral), + }); + for (const collateral of collaterals) { + collateralReturnValue = mergeValue( + collateralReturnValue, + toValue(collateral.txIn.amount!), + ); + } + const collateralReturn = new TransactionOutput( + toCardanoAddress(collateralReturnAddress), + collateralReturnValue, + ); + this.txBody.setCollateralReturn(collateralReturn); + }; + private setValidityInterval = (validity: ValidityRange) => { if (validity.invalidBefore) { this.txBody.setValidityStartInterval(Slot(validity.invalidBefore)); diff --git a/packages/mesh-core-cst/test/ref-script-input-fee-calc.test.ts b/packages/mesh-core-cst/test/ref-script-input-fee-calc.test.ts index b243bcd3..07881736 100644 --- a/packages/mesh-core-cst/test/ref-script-input-fee-calc.test.ts +++ b/packages/mesh-core-cst/test/ref-script-input-fee-calc.test.ts @@ -4,7 +4,7 @@ import { CardanoSDKSerializer, Transaction, TxCBOR } from "@meshsdk/core-cst"; describe("Ref script inputs", () => { it("Basic ref script inputs should be included in fees", async () => { - const body: MeshTxBuilderBody = { + const body: Partial = { inputs: [ { type: "PubKey", @@ -124,7 +124,6 @@ describe("Ref script inputs", () => { strategy: "experimental", includeTxFees: false, }, - fee: "0", expectedNumberKeyWitnesses: 0, expectedByronAddressWitnesses: [], }; diff --git a/packages/mesh-core-cst/test/ref-script-output.test.ts b/packages/mesh-core-cst/test/ref-script-output.test.ts index 4195dbed..7aa251f3 100644 --- a/packages/mesh-core-cst/test/ref-script-output.test.ts +++ b/packages/mesh-core-cst/test/ref-script-output.test.ts @@ -4,21 +4,8 @@ import { CardanoSDKSerializer, Transaction, TxCBOR } from "@meshsdk/core-cst"; describe("Ref Script output", () => { it("serializer should build a tx with ref script output", async () => { - const body: MeshTxBuilderBody = { - inputs: [ - { - type: "PubKey", - txIn: { - txHash: - "c15875aef7d14602040affbb80219681d318dfd035c03f9581cca5c625270e6a", - txIndex: 1, - amount: [{ unit: "lovelace", quantity: "307063809" }], - address: - "addr_test1qprgjf3u3tkl0qk9738jlyspg25dukxuzrz2lugmp7uypqzw6yxtdssjk4pxv9j489vv8ekkh03wvet9v2y2tsdl6cjs6dsvxs", - scriptSize: 0, - }, - }, - ], + const body: Partial = { + inputs: [], outputs: [ { address: @@ -76,13 +63,25 @@ describe("Ref Script output", () => { }, }, network: "preprod", - extraInputs: [], + extraInputs: [ + { + input: { + txHash: + "c15875aef7d14602040affbb80219681d318dfd035c03f9581cca5c625270e6a", + outputIndex: 1, + }, + output: { + amount: [{ unit: "lovelace", quantity: "307063809" }], + address: + "addr_test1qprgjf3u3tkl0qk9738jlyspg25dukxuzrz2lugmp7uypqzw6yxtdssjk4pxv9j489vv8ekkh03wvet9v2y2tsdl6cjs6dsvxs", + }, + }, + ], selectionConfig: { threshold: "", strategy: "experimental", includeTxFees: false, }, - fee: "0", expectedNumberKeyWitnesses: 0, expectedByronAddressWitnesses: [], }; diff --git a/packages/mesh-core/package.json b/packages/mesh-core/package.json index 5e64c489..e4c83624 100644 --- a/packages/mesh-core/package.json +++ b/packages/mesh-core/package.json @@ -1,6 +1,6 @@ { "name": "@meshsdk/core", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "description": "Mesh SDK Core - https://meshjs.dev/", "main": "./dist/index.cjs", "browser": "./dist/index.js", @@ -33,12 +33,12 @@ "typescript": "^5.3.3" }, "dependencies": { - "@meshsdk/common": "1.9.0-beta-39", - "@meshsdk/core-cst": "1.9.0-beta-39", - "@meshsdk/provider": "1.9.0-beta-39", - "@meshsdk/react": "1.9.0-beta-39", - "@meshsdk/transaction": "1.9.0-beta-39", - "@meshsdk/wallet": "1.9.0-beta-39" + "@meshsdk/common": "1.9.0-beta-40", + "@meshsdk/core-cst": "1.9.0-beta-40", + "@meshsdk/provider": "1.9.0-beta-40", + "@meshsdk/react": "1.9.0-beta-40", + "@meshsdk/transaction": "1.9.0-beta-40", + "@meshsdk/wallet": "1.9.0-beta-40" }, "prettier": "@meshsdk/configs/prettier", "publishConfig": { diff --git a/packages/mesh-provider/package.json b/packages/mesh-provider/package.json index 1b5b0fbf..6fa9ff9b 100644 --- a/packages/mesh-provider/package.json +++ b/packages/mesh-provider/package.json @@ -1,6 +1,6 @@ { "name": "@meshsdk/provider", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "description": "Blockchain data providers - https://meshjs.dev/providers", "main": "./dist/index.cjs", "browser": "./dist/index.js", @@ -35,8 +35,8 @@ "typescript": "^5.3.3" }, "dependencies": { - "@meshsdk/common": "1.9.0-beta-39", - "@meshsdk/core-cst": "1.9.0-beta-39", + "@meshsdk/common": "1.9.0-beta-40", + "@meshsdk/core-cst": "1.9.0-beta-40", "@utxorpc/sdk": "0.6.2", "@utxorpc/spec": "0.10.1", "axios": "^1.7.2" diff --git a/packages/mesh-react/package.json b/packages/mesh-react/package.json index e789b748..fbfc704d 100644 --- a/packages/mesh-react/package.json +++ b/packages/mesh-react/package.json @@ -1,6 +1,6 @@ { "name": "@meshsdk/react", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "description": "React component library - https://meshjs.dev/react", "main": "./dist/index.cjs", "browser": "./dist/index.js", @@ -30,9 +30,9 @@ }, "dependencies": { "@fabianbormann/cardano-peer-connect": "^1.2.18", - "@meshsdk/common": "1.9.0-beta-39", - "@meshsdk/transaction": "1.9.0-beta-39", - "@meshsdk/wallet": "1.9.0-beta-39", + "@meshsdk/common": "1.9.0-beta-40", + "@meshsdk/transaction": "1.9.0-beta-40", + "@meshsdk/wallet": "1.9.0-beta-40", "@meshsdk/web3-sdk": "0.0.26", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", diff --git a/packages/mesh-svelte/package.json b/packages/mesh-svelte/package.json index e3d7bbff..528e9f0c 100644 --- a/packages/mesh-svelte/package.json +++ b/packages/mesh-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@meshsdk/svelte", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "description": "Svelte component library - https://meshjs.dev/svelte", "type": "module", "exports": { @@ -26,7 +26,7 @@ "dev": "vite dev" }, "dependencies": { - "@meshsdk/core": "1.9.0-beta-39", + "@meshsdk/core": "1.9.0-beta-40", "bits-ui": "1.0.0-next.65" }, "devDependencies": { diff --git a/packages/mesh-transaction/package.json b/packages/mesh-transaction/package.json index c23320ef..0a0ebf8f 100644 --- a/packages/mesh-transaction/package.json +++ b/packages/mesh-transaction/package.json @@ -1,6 +1,6 @@ { "name": "@meshsdk/transaction", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "description": "Transactions - https://meshjs.dev/apis/transaction", "main": "./dist/index.cjs", "browser": "./dist/index.js", @@ -35,8 +35,8 @@ "typescript": "^5.3.3" }, "dependencies": { - "@meshsdk/common": "1.9.0-beta-39", - "@meshsdk/core-cst": "1.9.0-beta-39", + "@meshsdk/common": "1.9.0-beta-40", + "@meshsdk/core-cst": "1.9.0-beta-40", "@cardano-sdk/core": "^0.45.5", "@cardano-sdk/util": "^0.15.5", "@cardano-sdk/input-selection": "^0.13.33", diff --git a/packages/mesh-transaction/src/mesh-tx-builder/index.ts b/packages/mesh-transaction/src/mesh-tx-builder/index.ts index 6c3f89d9..6056fb01 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/index.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/index.ts @@ -11,6 +11,7 @@ import { ISubmitter, MeshTxBuilderBody, MintItem, + MintParam, Output, Protocol, ScriptSource, @@ -18,6 +19,7 @@ import { TxIn, UTxO, Vote, + Voter, Withdrawal, } from "@meshsdk/common"; import { @@ -26,6 +28,7 @@ import { toDRep as coreToCstDRep, Address as CstAddress, AddressType as CstAddressType, + CredentialCore as CstCredential, CredentialType as CstCredentialType, NativeScript as CstNativeScript, Script as CstScript, @@ -40,7 +43,6 @@ import { TransactionPrototype, } from "./coin-selection/coin-selection-interface"; import { MeshTxBuilderCore } from "./tx-builder-core"; -import {MintParam} from "@meshsdk/common"; export interface MeshTxBuilderOptions { fetcher?: IFetcher; @@ -147,6 +149,9 @@ export class MeshTxBuilder extends MeshTxBuilderCore { complete = async (customizedTx?: Partial) => { if (customizedTx) { this.meshTxBuilderBody = { ...this.meshTxBuilderBody, ...customizedTx }; + if (customizedTx.fee) { + this.setFee(customizedTx.fee); + } } else { this.queueAllLastItem(); } @@ -167,11 +172,12 @@ export class MeshTxBuilder extends MeshTxBuilderCore { collateral.txIn.scriptSize = 0; } await this.completeTxParts(); - this.sortTxParts(); + await this.sanitizeOutputs(); const txPrototype = await this.selectUtxos(); await this.updateByTxPrototype(txPrototype, true); this.queueAllLastItem(); this.removeDuplicateInputs(); + this.sortTxParts(); if (this.verbose) { console.log( "txBodyJson - after coin selection", @@ -182,6 +188,7 @@ export class MeshTxBuilder extends MeshTxBuilderCore { }), ); } + const txHex = this.serializer.serializeTxBody( this.meshTxBuilderBody, this._protocolParams, @@ -216,7 +223,7 @@ export class MeshTxBuilder extends MeshTxBuilderCore { throw new Error(`Evaluate redeemers failed: ${String(error)}`); } } - const fee = clonedBuilder.calculateFee(); + const fee = clonedBuilder.getActualFee(); const redeemers = clonedBuilder.getRedeemerCosts(); return { fee, @@ -289,10 +296,7 @@ export class MeshTxBuilder extends MeshTxBuilderCore { this.txOut(change.address, change.amount); } - this.meshTxBuilderBody.fee = - this.meshTxBuilderBody.fee === "0" - ? selectionSkeleton.fee.toString() - : this.meshTxBuilderBody.fee; + this.meshTxBuilderBody.fee = selectionSkeleton.fee.toString(); this.updateRedeemer( this.meshTxBuilderBody, selectionSkeleton.redeemers ?? [], @@ -313,6 +317,13 @@ export class MeshTxBuilder extends MeshTxBuilderCore { }; sortTxParts = () => { + this.sortInputs(); + this.sortMints(); + this.sortWithdrawals(); + this.sortVotes(); + }; + + sortInputs = () => { // Sort inputs based on txHash and txIndex this.meshTxBuilderBody.inputs.sort((a, b) => { if (a.txIn.txHash < b.txIn.txHash) return -1; @@ -321,8 +332,10 @@ export class MeshTxBuilder extends MeshTxBuilderCore { if (a.txIn.txIndex > b.txIn.txIndex) return 1; return 0; }); + }; - // Sort mints based on policy id and asset name + sortMints = () => { + // Sort mints based on policy id this.meshTxBuilderBody.mints.sort((a, b) => { if (a.policyId < b.policyId) return -1; if (a.policyId > b.policyId) return 1; @@ -330,6 +343,119 @@ export class MeshTxBuilder extends MeshTxBuilderCore { }); }; + protected compareCredentials = ( + credentialA: CstCredential, + credentialB: CstCredential, + ): number => { + // Script credentials come before Key credentials + if ( + credentialA.type === CstCredentialType.ScriptHash && + credentialB.type === CstCredentialType.KeyHash + ) { + return -1; + } + if ( + credentialA.type === CstCredentialType.KeyHash && + credentialB.type === CstCredentialType.ScriptHash + ) { + return 1; + } + // If same type, compare the hashes + if (credentialA.type === credentialB.type) { + if (credentialA.hash < credentialB.hash) return -1; + if (credentialA.hash > credentialB.hash) return 1; + return 0; + } + return 0; + }; + + sortWithdrawals = () => { + this.meshTxBuilderBody.withdrawals.sort((a, b) => { + const credentialA = CstAddress.fromString(a.address) + ?.asReward() + ?.getPaymentCredential(); + const credentialB = CstAddress.fromString(b.address) + ?.asReward() + ?.getPaymentCredential(); + if (credentialA && credentialB) { + return this.compareCredentials(credentialA, credentialB); + } + return 0; + }); + }; + + sortVotes = () => { + const variantOrder: Record = { + ConstitutionalCommittee: 0, + DRep: 1, + StakingPool: 2, + }; + this.meshTxBuilderBody.votes.sort((a, b) => { + const voterA = a.vote.voter; + const voterB = b.vote.voter; + const orderA = variantOrder[voterA.type]; + const orderB = variantOrder[voterB.type]; + if (orderA !== orderB) return orderA - orderB; + + // Same variant, compare inner values + if ( + voterA.type === "ConstitutionalCommittee" && + voterB.type === "ConstitutionalCommittee" + ) { + const credA = voterA.hotCred; + const credB = voterB.hotCred; + // Script credentials come before Key credentials + if (credA.type === "ScriptHash" && credB.type === "KeyHash") { + return -1; + } + if (credA.type === "KeyHash" && credB.type === "ScriptHash") { + return 1; + } + // If same type, compare the hashes + if (credA.type === credB.type) { + const hashA = + credA.type === "KeyHash" ? credA.keyHash : credA.scriptHash; + const hashB = + credB.type === "KeyHash" ? credB.keyHash : credB.scriptHash; + if (hashA < hashB) return -1; + if (hashA > hashB) return 1; + return 0; + } + return 0; + } + if (voterA.type === "DRep" && voterB.type === "DRep") { + const drepA = coreToCstDRep(voterA.drepId); + const drepB = coreToCstDRep(voterB.drepId); + const scriptHashA = drepA.toScriptHash(); + const scriptHashB = drepB.toScriptHash(); + const keyHashA = drepA.toKeyHash(); + const keyHashB = drepB.toKeyHash(); + + // Script hashes come before key hashes + if (scriptHashA != null && scriptHashB != null) { + if (scriptHashA < scriptHashB) return -1; + if (scriptHashA > scriptHashB) return 1; + return 0; + } + if (scriptHashA != null) return -1; + if (scriptHashB != null) return 1; + // If both are key hashes, compare them + if (keyHashA != null && keyHashB != null) { + if (keyHashA < keyHashB) return -1; + if (keyHashA > keyHashB) return 1; + return 0; + } + return 0; + } + if (voterA.type === "StakingPool" && voterB.type === "StakingPool") { + if (voterA.keyHash < voterB.keyHash) return -1; + if (voterA.keyHash > voterB.keyHash) return 1; + return 0; + } + return 0; + }); + }; + evaluateRedeemers = async () => { let txHex = this.serializer.serializeTxBody( this.meshTxBuilderBody, @@ -429,6 +555,9 @@ export class MeshTxBuilder extends MeshTxBuilderCore { completeUnbalancedSync = (customizedTx?: MeshTxBuilderBody) => { if (customizedTx) { this.meshTxBuilderBody = customizedTx; + if (customizedTx.fee) { + this.setFee(customizedTx.fee); + } } else { this.queueAllLastItem(); } @@ -816,6 +945,24 @@ export class MeshTxBuilder extends MeshTxBuilderCore { this.sortTxParts(); }; + protected sanitizeOutputs = async () => { + this.meshTxBuilderBody.outputs.forEach((output) => { + let lovelaceFound = false; + output.amount.forEach((asset) => { + if (asset.unit === "lovelace" || asset.unit === "") { + lovelaceFound = true; + } + }); + if (!lovelaceFound) { + const minAda = this.calculateMinLovelaceForOutput(output); + output.amount.push({ + unit: "lovelace", + quantity: minAda.toString(), + }); + } + }); + }; + protected collectAllRequiredSignatures = (): { keyHashes: Set; byronAddresses: Set; @@ -917,7 +1064,11 @@ export class MeshTxBuilder extends MeshTxBuilderCore { } } else if (certType.type === "RetirePool") { certCreds.add(certType.poolId); - } else if (certType.type === "DRepRegistration") { + } else if ( + certType.type === "DRepRegistration" || + certType.type === "DRepDeregistration" || + certType.type === "DRepUpdate" + ) { if (cert.type === "BasicCertificate") { const cstDrep = coreToCstDRep(certType.drepId); const keyHash = cstDrep.toKeyHash(); @@ -934,6 +1085,10 @@ export class MeshTxBuilder extends MeshTxBuilderCore { certType.type === "StakeRegistrationAndDelegation" || certType.type === "VoteRegistrationAndDelegation" || certType.type === "StakeVoteRegistrationAndDelegation" || + certType.type === "VoteDelegation" || + certType.type === "RegisterStake" || + certType.type === "StakeAndVoteDelegation" || + certType.type === "DelegateStake" || certType.type === "DeregisterStake" ) { if (cert.type === "BasicCertificate") { @@ -1023,7 +1178,7 @@ export class MeshTxBuilder extends MeshTxBuilderCore { } } return mintCreds; - } + }; protected getTotalWithdrawal = (): bigint => { let accum = 0n; @@ -1474,14 +1629,10 @@ export class MeshTxBuilder extends MeshTxBuilderCore { } } memUnits = BigInt( - new BigNumber(memUnits) - .integerValue(BigNumber.ROUND_CEIL) - .toString(), + new BigNumber(memUnits).integerValue(BigNumber.ROUND_CEIL).toString(), ); stepUnits = BigInt( - new BigNumber(stepUnits) - .integerValue(BigNumber.ROUND_CEIL) - .toString(), + new BigNumber(stepUnits).integerValue(BigNumber.ROUND_CEIL).toString(), ); return { memUnits, @@ -1493,6 +1644,14 @@ export class MeshTxBuilder extends MeshTxBuilderCore { return this.serializeMockTx().length / 2; }; + getActualFee = (): bigint => { + if (this.manualFee) { + return BigInt(this.manualFee); + } else { + return this.calculateFee(); + } + }; + calculateFee = (): bigint => { const txSize = this.getSerializedSize(); return this.calculateFeeForSerializedTx(txSize); @@ -1533,7 +1692,7 @@ export class MeshTxBuilder extends MeshTxBuilderCore { this.serializer.serializeOutput(currentOutput).length / 2, ); const txOutByteCost = BigInt(this._protocolParams.coinsPerUtxoSize); - const totalOutCost = 160n + BigInt(txOutSize) * txOutByteCost; + const totalOutCost = (160n + BigInt(txOutSize)) * txOutByteCost; minAda = totalOutCost; if (lovelace < totalOutCost) { lovelace = totalOutCost; @@ -1643,7 +1802,7 @@ const setLoveLace = (output: Output, lovelace: bigint): Output => { const getLovelace = (output: Output): bigint => { for (let asset of output.amount) { - if (asset.unit === "lovelace") { + if (asset.unit === "lovelace" || asset.unit === "") { return BigInt(asset.quantity); } } diff --git a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts index 2c459e16..d09b94cc 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts @@ -53,6 +53,8 @@ export class MeshTxBuilderCore { private addingPlutusVote = false; private plutusVoteScriptVersion: LanguageVersion | undefined; + protected manualFee: string | undefined; + protected _protocolParams: Protocol = DEFAULT_PROTOCOL_PARAMETERS; protected mintItem?: MintItem; @@ -1489,10 +1491,32 @@ export class MeshTxBuilderCore { * @returns The MeshTxBuilder instance */ setFee = (fee: string) => { + this.manualFee = fee; this.meshTxBuilderBody.fee = fee; return this; }; + /** + * Sets a total collateral for the transaction to use, a collateral return + * will be generated to either the change address or the specified collateral return address + * @param collateral The specified collateral + * @returns The MeshTxBuilder instance + */ + setTotalCollateral = (collateral: string) => { + this.meshTxBuilderBody.totalCollateral = collateral; + return this; + }; + + /** + * Sets the collateral return address, if none is set, the change address will be used + * @param address The address to use for collateral return + * @returns The MeshTxBuilder instance + */ + setCollateralReturnAddress = (address: string) => { + this.meshTxBuilderBody.collateralReturnAddress = address; + return this; + }; + /** * Sets the network to use, this is mainly to know the cost models to be used to calculate script integrity hash * @param network The specific network this transaction is being built for ("testnet" | "preview" | "preprod" | "mainnet") @@ -1721,7 +1745,9 @@ export class MeshTxBuilderCore { txEvaluation: Omit[], doNotUseMultiplier: boolean = false, ) => { - const txEvaluationMultiplier = doNotUseMultiplier ? 1 : this.txEvaluationMultiplier; + const txEvaluationMultiplier = doNotUseMultiplier + ? 1 + : this.txEvaluationMultiplier; txEvaluation.forEach((redeemerEvaluation) => { switch (redeemerEvaluation.tag) { case "SPEND": { @@ -1836,7 +1862,6 @@ export class MeshTxBuilderCore { } return map; }, requiredAssets); - const selectionConfig = this.meshTxBuilderBody.selectionConfig; const utxoSelection = new UtxoSelection( @@ -2005,6 +2030,7 @@ export class MeshTxBuilderCore { newBuilder.refScriptTxInQueueItem = this.refScriptTxInQueueItem ? structuredClone(this.refScriptTxInQueueItem) : undefined; + newBuilder.manualFee = this.manualFee; return newBuilder; } diff --git a/packages/mesh-transaction/test/mesh-tx-builder/balance.test.ts b/packages/mesh-transaction/test/mesh-tx-builder/balance.test.ts index 243158e2..00d56d8c 100644 --- a/packages/mesh-transaction/test/mesh-tx-builder/balance.test.ts +++ b/packages/mesh-transaction/test/mesh-tx-builder/balance.test.ts @@ -1,4 +1,7 @@ +import { MeshTxBuilderBody } from "@meshsdk/common"; +import { OfflineEvaluator } from "@meshsdk/core-csl"; import { Transaction, TxCBOR } from "@meshsdk/core-cst"; +import { BlockfrostProvider, OfflineFetcher } from "@meshsdk/provider"; import { MeshTxBuilder } from "@meshsdk/transaction"; import { calculateOutputLovelaces, txHash } from "../test-util"; @@ -107,4 +110,316 @@ describe("MeshTxBuilder", () => { .complete(), ).rejects.toThrow("UTxO Fully Depleted"); }); + + it("Transaction should balance even when outputs don't have lovelaces", async () => { + const fetcher = new OfflineFetcher("preprod"); + + fetcher.addUTxOs([ + { + input: { + txHash: + "a4f8080e8e34992977fae292c0f5d843c6c78f837fa69c624e66b6aab868745e", + outputIndex: 0, + }, + output: { + amount: [ + { unit: "lovelace", quantity: "1241280" }, + { + unit: "c76c35088ac826c8a0e6947c8ff78d8d4495789bc729419b3a33430520", + quantity: "1", + }, + ], + address: + "addr_test1xrrkcdgg3tyzdj9qu628erlh3kx5f9tcn0rjjsvm8ge5xpvke8x9mpjf7aerjt3n3nfd5tnzkfhlprp09mpf4sdy8dzqs6wq6p", + }, + }, + { + input: { + txHash: + "67b37a5089df9e785aa9c22ac68788e870d8dde85d1df66f2cbbe86721ecd9a0", + outputIndex: 0, + }, + output: { + amount: [{ unit: "lovelace", quantity: "100000000" }], + address: + "addr_test1qqa9lwdqap8qm9desuq783a5g0xuseuvrkjjd775ajk5ktvjc8vghyqzuh2suqp2rf0e35zx9rjhkqmpq8tmx9q0zq5ssffpud", + }, + }, + { + input: { + txHash: + "70f7f43ead6d2e4bb7c41ccda408dfc23c29c3493560a9ebb7005c051ffb7e65", + outputIndex: 0, + }, + output: { + amount: [{ unit: "lovelace", quantity: "5000000" }], + address: + "addr_test1qqa9lwdqap8qm9desuq783a5g0xuseuvrkjjd775ajk5ktvjc8vghyqzuh2suqp2rf0e35zx9rjhkqmpq8tmx9q0zq5ssffpud", + }, + }, + { + input: { + txHash: + "4df3ebc0592b39124c5cc3a1cf680a5d7ac393531dd308e34ee499fbad7257e7", + outputIndex: 7, + }, + output: { + amount: [ + { + unit: "lovelace", + quantity: "13391170", + }, + { + unit: "3ff4fba4491339dc471538763a7d87df527757473a0c70b017400730496e64657853637269707473", + quantity: "1", + }, + ], + address: + "addr_test1xqzp3cel9p95shlsaertqrt0wcmym5y780ljmca80k4u8ruke8x9mpjf7aerjt3n3nfd5tnzkfhlprp09mpf4sdy8dzqdxy5xn", + plutusData: + "d8799fd8799fd8799f581c78e4d6294634e3472d53f1a7236ad7967a90266e1ea00db3902ce42fffd8799fd8799fd8799f581ce6edaec7254d30a009e38970c7de292bdb3c5b5e8df95b20565f244dffffffffd879809fd8799f40401a004c4b40ffff9fd87a9f581c3cd70530341026cdd03d8d38f53d5993503bf125f2dad58edd377c99ffffff", + scriptRef: + "590a7401010033232323232323232323232323232323232323232323232322290029111919192c99192a999ab9a3370e90000008899191919191919191919191919480bc88c8a403e444644652601001d223255333573466e1d2006330260044800044c964cc005301024120004c010101008ac998009ba93371491103313030000184c10101008ac998009ba93371491103323232000184c10101008ac9803808c4c8c964c008cc00401806a2b260046600203400b15955333573466e4001806844ccd5cd19b9001a005800400846005133011008480023002460048c0084466e95200433574066e9520003357406ea4008cd5d01ba90014bd7025eb808cc0f88954ccd5cd1aba300111611325955333573466ebcd5d09aab9e375400200c22666ae68cdd798168008024002004230028aa999ab9a3375e980103d87a8000302e00111325932593300101d4c1024120008c0022660020269801024120001818000c4ccd5cd19b8748010cc0ac0052000800400a3002181f80088c00a260066ae880086ae8400404a3002460048c00918010c0c0010460046604c02a01233028375601c024606c44aa666ae68d5d180088b08992a999ab9a3375e6ae84d55cf1baa0010061132593302b0050018aa999ab9a3370e66046002900019816002a400022aa666ae68cdd7a6103d87a8000302700211333573466ebc018c09800a0010021180108c00a3002181b8008898019aba2002357420023031225533357346ae8c00445844c8c96564cc00403d301024120008c00226600200a98010241200044c8c8c954ccd5cd19b8748000008460026eb8d5d0800cdd71aba13574400211635573c0046aae74004dd51aba135573c6ea8c0b800a260086ae8800c60446eacc0c0004d5d08008c0b88896400a2666ae68cdc3a4004003000801488564cc09801800a26600a00266e01200200389980280080186eacd5d09aba23574400860420266eb0d5d08049aba1001357440026ae88004d5d10021aba135573c00c6eb8d5d0803180f99980e1bac357420020149810d4c496e646578536372697074730035744002603001222aa666ae68cdc3a400400222646660246ae84d55cf1baa357426aae78dd51aba135573c6ea8d5d09aba235573c6ea8cc068dd61aba1001357426aae7800cdd59aba1357446ae88d5d11aba2357446ae88d5d11aba2357440029000180c00488aa999ab9a3370e90020008899191919194807c88a401e446464446600601697ae0302d22232325533357346ae8c01044cc0040312f5c02264b2aa666ae68cdd79aba135573c6ea8004cc078dd480780508992c9804000c554ccd5cd19b8748010cc074005200011333573466ebd300103d87a8000302100280040084600518010c0c4004460051330063574400a66ae8000401226600c6ae880140106ae84010c0c4888c954ccd5cd1aba3003115930010108980280146004113259300a375660640031330053574400866ae80d5d09aba235573c6ea800400e26600a6ae8801000c6ae8400cc0d088c8c954ccd5cd1aba3003116113255333573466ebcd5d09aab9e375400201e2264aa666ae68cdc419981c800a6010140004c0101400033702006008230021155333573466ebcc09800803c4554ccd5cd19baf4c103d87a8000302700211333573466e1d200233024001480020010021180108c008c0dc00444c014d5d10021aba10033303400448000cc0cc01d20003030225533357346ae8c00446000226464646606a44aa666ae68d5d180088b08992a999ab9a3375e604800200a22aa666ae68cdc419981b981b000a6010140004c010140003330370044c10140004c010140001130083574400e22c2260066ae88008d5d08008031819001180f8009aba100125933301b0010054c01024120008c0022666ae68cdc3a40046660580026ea402530010241200080040081bac3574200860340186eb8d5d09aab9e37546ae84d55cf002180f19980d9bac357426ae880040253010d4c496e646578536372697074730030180091155333573466e1d20060011180008b1aab9d00137546ae84d5d1000c5268b0d5d10009aab9e001375400830172225533357346ae8c008440044554ccd5cd19b8748008cc010dd59aab9e35742004900008998019aba200233700900100088b180b1112a999ab9a357460042200222660066ae88008cdc0240040024466e95200032335740600200666ae80cdd2a400066ae80cdd2a400066ae80c0040092f5c097ae04bd70119ba548008cd5d0000a5eb808d5d09aba2357446aae78dd500091aba1357446ae88d5d11aab9e3754002444666ae68cdc3a4004666028006004003000801180891112a999ab9a3574600422666ae68cdc3a4004003000801089919192a999ab9a3375e00266e952000002113330070063574400a66e0120020041155333573466ebc004cdd2a400866ae80cdd2a400400497ae0113330070063574400a66e012002004113330070063574400a0086aae74008cd5d000225eb80d5d080118081112a999ab9a3574600422c2264aa666ae68cdd79aab9d0010021137566aae7800444cc010d5d10018011aba1002300f222590028c00244265200722255333573466e2001000445844cc02401401c6660260072005222375200690029111ba90020c0388896400a2003221330050013370090010018c03488954ccd5cd1aba30021180108aa999ab9a3375e6aae74d5d080100088c00044cc00cd5d1001000980611112a999ab9a3574600622c2264aa666ae68cdd79aab9d00100311333573466ebcd55cf00080140020042266600a6ae8801000c008d5d080191bac357426ae88d5d1180100091aab9e37546ae84d55cf1baa00130092225533357346ae8c00845844c954ccd5cd19baf357426aae78dd50008010880088998021aba2003002357420044446ae84d55cf1baa30043330050030020012357426ae88d5d11aab9e37546ae84d5d11aab9e3754002600c4444aa666ae68d5d180188b08992a999ab9a3370e90011998051bab30070010030021100111333005357440080060046ae8400c8c8c8c954ccd5cd19b874800000844c8c8ca0026ae840126ae8400e660024646464aa666ae68cdc3a40000042265001375c6ae8400a6eb8d5d0800cdd69aba1357440023574400222c6aae78008d55ce8009baa00135742005330012001357426ae88008464460046eb0004c03088cccd55cf80094000a00660086ae8400a60066ae88009000357440026ae88004458d55cf0011aab9d001375400246ae84d5d11aab9e37546ae84d5d11aab9e37540026006444aa666ae68d5d18010880088998019aba20023370066600a60086ae840093010140004c01014000001237566ae84d5d11aab9e3754002444646600a44aa666ae68d5d180088a40002264aa666ae68cdd79aab9d00100611300437566aae7800444c00cd5d10011aba10010043004225533357346ae8c004452000113255333573466ebcd55ce800802089bad35573c0022260066ae88008d5d080091918008009180111980100100099999a891001111400400e005001109480140049811e581c3ff4fba4491339dc471538763a7d87df527757473a0c70b017400730004c011e581c123f0263f0bd818fb43bdba5362e72bcb594e2b48265a8a7ef93fe07004c011e581c96c9cc5d8649f772392e338cd2da2e62b26ff08c2f2ec29ac1a43b440001", + }, + }, + { + input: { + txHash: + "8a3a9c393bec05d40b73ed459a10a5c9c7a11f197c88d1aaca48080a2e48e7c5", + outputIndex: 1, + }, + output: { + address: + "addr_test1qrk3ahz8vfyudwxzkk900vyh4kwvvkupezuztxs2uhxegx33r7tjl95j976frv930fsr4e3fnzff66tglgwjzva3f8vsas3rxn", + amount: [ + { + unit: "lovelace", + quantity: "4111740", + }, + ], + scriptRef: + "5902c9010100332323232322232323259323255333573466e1d20040011132329009914802488c8c8c8c954ccd5cd19baf3574200200e226464aa666ae68cdd79aba10010051155333573466ebd30103d87a8000357426ae8800444ccd5cd19b8748010cc050dd59aba1002480020010021180108c008d5d10009aba2001118011aab9e001375400466e95200433574066e9520003357406ea4dd700299aba0375200666ae80dd39808c000cd5d000125eb812f5c066601e00a0086ea4cdc5244103313030000020dd61aba1357446ae88d55cf1baa3574200a646464aa666ae68cdc3a400000423001375c6ae840066ae84d5d10008458d55cf0011aab9d00137546ae8400c4554ccd5cd19b87480180044600022c6aae74004dd51aba135744003149a2c357440026aae78004dd500098021112a999ab9a357460042200222660066ae88008cdc024004002600644446464aa666ae68d5d180288b08992a999ab9a3370e900118011bab357426ae88d55cf1baa00111001113330073574400c00a0086ae84014c0208954ccd5cd1aba3001114800044c954ccd5cd19baf35573a00200c2260086eacd55cf0008898019aba200235742002600e44aa666ae68d5d180088a40002264aa666ae68cdd79aab9d00100411375a6aae7800444c00cd5d10011aba10013002229001a5eb824466ae82400a44466e9520003357406ea400ccd5d01ba73008002335740b200314c0103d87a80008a6103d879800025eb8060080023001229001a5eb824466ae80dd4801180200088c8c0040048c0088cc008008004cccd44880088a002005001109480140049811e581cc76c35088ac826c8a0e6947c8ff78d8d4495789bc729419b3a334305004c0150d8799fd87a9f581cfc683d569387e6452300cd5c9b3d1d9b49ecd4ec2f714254fc672ef8ffd8799fd8799fd87a9f581c96c9cc5d8649f772392e338cd2da2e62b26ff08c2f2ec29ac1a43b44ffffffff0001", + }, + }, + ]); + + const txBody: Partial = { + inputs: [ + { + type: "Script", + txIn: { + txHash: + "a4f8080e8e34992977fae292c0f5d843c6c78f837fa69c624e66b6aab868745e", + txIndex: 0, + amount: [ + { unit: "lovelace", quantity: "1241280" }, + { + unit: "c76c35088ac826c8a0e6947c8ff78d8d4495789bc729419b3a33430520", + quantity: "1", + }, + ], + address: + "addr_test1xrrkcdgg3tyzdj9qu628erlh3kx5f9tcn0rjjsvm8ge5xpvke8x9mpjf7aerjt3n3nfd5tnzkfhlprp09mpf4sdy8dzqs6wq6p", + scriptSize: 0, + }, + scriptTxIn: { + datumSource: { + type: "Inline", + txHash: + "a4f8080e8e34992977fae292c0f5d843c6c78f837fa69c624e66b6aab868745e", + txIndex: 0, + }, + scriptSource: { + type: "Inline", + txHash: + "4df3ebc0592b39124c5cc3a1cf680a5d7ac393531dd308e34ee499fbad7257e7", + txIndex: 7, + scriptHash: + "c76c35088ac826c8a0e6947c8ff78d8d4495789bc729419b3a334305", + version: "V3", + scriptSize: "2684", + }, + redeemer: { + data: { type: "JSON", content: '{"constructor":0,"fields":[]}' }, + exUnits: { mem: 86660, steps: 29884994 }, + }, + }, + }, + ], + outputs: [ + { + address: + "addr_test1qpuwf43fgc6wx3ed20c6wgm267t84ypxdc02qrdnjqkwgtlxakhvwf2dxzsqncufwrrau2ftmv79kh5dl9djq4jly3xspgyfcz", + amount: [{ unit: "lovelace", quantity: "5000000" }], + }, + { + address: + "addr_test1xrrkcdgg3tyzdj9qu628erlh3kx5f9tcn0rjjsvm8ge5xpvke8x9mpjf7aerjt3n3nfd5tnzkfhlprp09mpf4sdy8dzqs6wq6p", + amount: [ + { + unit: "c76c35088ac826c8a0e6947c8ff78d8d4495789bc729419b3a33430520", + quantity: "1", + }, + ], + datum: { + type: "Inline", + data: { + type: "JSON", + content: + '{"constructor":0,"fields":[{"bytes":"67656e65736973"},{"bytes":"676f64"}]}', + }, + }, + }, + { + address: + "addr_test1xrrkcdgg3tyzdj9qu628erlh3kx5f9tcn0rjjsvm8ge5xpvke8x9mpjf7aerjt3n3nfd5tnzkfhlprp09mpf4sdy8dzqs6wq6p", + amount: [ + { + unit: "c76c35088ac826c8a0e6947c8ff78d8d4495789bc729419b3a33430520", + quantity: "1", + }, + ], + datum: { + type: "Inline", + data: { + type: "JSON", + content: + '{"constructor":0,"fields":[{"bytes":"676f64"},{"bytes":"67756c6c6130"}]}', + }, + }, + }, + { + address: + "addr_test1xr7xs02kjwr7v3frqrx4exearkd5nmx5ashhzsj5l3nja7yke8x9mpjf7aerjt3n3nfd5tnzkfhlprp09mpf4sdy8dzq6ptcdp", + amount: [ + { + unit: "c76c35088ac826c8a0e6947c8ff78d8d4495789bc729419b3a334305313030676f64", + quantity: "1", + }, + ], + datum: { + type: "Inline", + data: { + type: "JSON", + content: + '{"constructor":0,"fields":[{"bytes":"c76c35088ac826c8a0e6947c8ff78d8d4495789bc729419b3a334305"},{"bytes":"676f64"},{"list":[]},{"bytes":"20"}]}', + }, + }, + }, + ], + collaterals: [ + { + type: "PubKey", + txIn: { + txHash: + "70f7f43ead6d2e4bb7c41ccda408dfc23c29c3493560a9ebb7005c051ffb7e65", + txIndex: 0, + amount: [{ unit: "lovelace", quantity: "5000000" }], + address: + "addr_test1qqa9lwdqap8qm9desuq783a5g0xuseuvrkjjd775ajk5ktvjc8vghyqzuh2suqp2rf0e35zx9rjhkqmpq8tmx9q0zq5ssffpud", + scriptSize: 0, + }, + }, + ], + requiredSignatures: [], + referenceInputs: [], + mints: [ + { + type: "Plutus", + policyId: "c76c35088ac826c8a0e6947c8ff78d8d4495789bc729419b3a334305", + scriptSource: { + type: "Inline", + txHash: + "4df3ebc0592b39124c5cc3a1cf680a5d7ac393531dd308e34ee499fbad7257e7", + txIndex: 7, + version: "V3", + scriptSize: "2684", + scriptHash: + "c76c35088ac826c8a0e6947c8ff78d8d4495789bc729419b3a334305", + }, + redeemer: { + data: { type: "JSON", content: '{"bytes":"676f64"}' }, + exUnits: { mem: 474805, steps: 166687990 }, + }, + mintValue: [ + { assetName: "20", amount: "1" }, + { assetName: "313030676f64", amount: "1" }, + { assetName: "323232676f64", amount: "1" }, + ], + }, + ], + changeAddress: + "addr_test1qqa9lwdqap8qm9desuq783a5g0xuseuvrkjjd775ajk5ktvjc8vghyqzuh2suqp2rf0e35zx9rjhkqmpq8tmx9q0zq5ssffpud", + metadata: new Map(), + validityRange: {}, + certificates: [], + withdrawals: [ + { + type: "ScriptWithdrawal", + address: + "stake_test17q7dwpfsxsgzdnws8kxn3afatxf4qwl3yhed44vwm5mhexgr3a09v", + coin: "0", + scriptSource: { + type: "Inline", + txHash: + "8a3a9c393bec05d40b73ed459a10a5c9c7a11f197c88d1aaca48080a2e48e7c5", + txIndex: 1, + scriptHash: + "3cd70530341026cdd03d8d38f53d5993503bf125f2dad58edd377c99", + version: "V3", + scriptSize: "721", + }, + redeemer: { + data: { + type: "JSON", + content: + '{"constructor":0,"fields":[{"bytes":"676f64"},{"bytes":"20"}]}', + }, + exUnits: { mem: 146930, steps: 51719571 }, + }, + }, + ], + votes: [], + signingKey: [], + chainedTxs: [], + inputsForEvaluation: { + "67b37a5089df9e785aa9c22ac68788e870d8dde85d1df66f2cbbe86721ecd9a00": { + input: { + outputIndex: 0, + txHash: + "67b37a5089df9e785aa9c22ac68788e870d8dde85d1df66f2cbbe86721ecd9a0", + }, + output: { + address: + "addr_test1qqa9lwdqap8qm9desuq783a5g0xuseuvrkjjd775ajk5ktvjc8vghyqzuh2suqp2rf0e35zx9rjhkqmpq8tmx9q0zq5ssffpud", + amount: [{ unit: "lovelace", quantity: "100000000" }], + }, + }, + }, + network: "mainnet", + expectedNumberKeyWitnesses: 0, + expectedByronAddressWitnesses: [], + extraInputs: [ + { + input: { + txHash: + "67b37a5089df9e785aa9c22ac68788e870d8dde85d1df66f2cbbe86721ecd9a0", + outputIndex: 0, + }, + output: { + amount: [{ unit: "lovelace", quantity: "100000000" }], + address: + "addr_test1qqa9lwdqap8qm9desuq783a5g0xuseuvrkjjd775ajk5ktvjc8vghyqzuh2suqp2rf0e35zx9rjhkqmpq8tmx9q0zq5ssffpud", + }, + }, + ], + }; + const newTxBuilder = new MeshTxBuilder({ fetcher }); + const txHex = await newTxBuilder.complete(txBody); + const cardanoTx = Transaction.fromCbor(TxCBOR(txHex)); + console.log(txHex); + expect(calculateOutputLovelaces(txHex) + cardanoTx.body().fee()).toEqual( + BigInt(1241280) + BigInt(100000000), + ); + }); }); diff --git a/packages/mesh-transaction/test/mesh-tx-builder/coin-selection.test.ts b/packages/mesh-transaction/test/mesh-tx-builder/coin-selection.test.ts index d9c332d5..2277913d 100644 --- a/packages/mesh-transaction/test/mesh-tx-builder/coin-selection.test.ts +++ b/packages/mesh-transaction/test/mesh-tx-builder/coin-selection.test.ts @@ -474,7 +474,7 @@ describe("MeshTxBuilder - coin selection", () => { amount: [ { unit: "lovelace", - quantity: "1000000", + quantity: "2000000", }, ], }, @@ -702,7 +702,7 @@ describe("MeshTxBuilder - coin selection", () => { amount: [ { unit: "lovelace", - quantity: "2000000", + quantity: "4000000", }, ...tokens, ], diff --git a/packages/mesh-transaction/test/mesh-tx-builder/collateral-return.test.ts b/packages/mesh-transaction/test/mesh-tx-builder/collateral-return.test.ts new file mode 100644 index 00000000..337ef9a0 --- /dev/null +++ b/packages/mesh-transaction/test/mesh-tx-builder/collateral-return.test.ts @@ -0,0 +1,150 @@ +import { MeshTxBuilder, serializePlutusScript } from "@meshsdk/core"; +import { + AssetId, + empty, + resolveScriptRef, + Serialization, + subValue, +} from "@meshsdk/core-cst"; +import { OfflineFetcher } from "@meshsdk/provider"; + +import { + alwaysSucceedCbor, + alwaysSucceedHash, + mockTokenUnit, + txHash, +} from "../test-util"; + +describe("MeshTxBuilder - collateral return test", () => { + const offlineFetcher = new OfflineFetcher(); + + offlineFetcher.addUTxOs([ + { + input: { + txHash: txHash("tx1"), + outputIndex: 0, + }, + output: { + address: + "addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9", + amount: [ + { + unit: "lovelace", + quantity: "100000000", + }, + { + unit: mockTokenUnit(0), + quantity: "1000000", + }, + { + unit: mockTokenUnit(1), + quantity: "10000", + }, + ], + }, + }, + { + input: { + txHash: txHash("tx2"), + outputIndex: 0, + }, + output: { + address: serializePlutusScript({ + code: alwaysSucceedCbor, + version: "V3", + }).address, + amount: [ + { + unit: "lovelace", + quantity: "100000000", + }, + ], + }, + }, + { + input: { + txHash: txHash("tx3"), + outputIndex: 0, + }, + output: { + address: serializePlutusScript({ + code: alwaysSucceedCbor, + version: "V3", + }).address, + amount: [ + { + unit: "lovelace", + quantity: "100000000", + }, + ], + scriptHash: alwaysSucceedHash, + scriptRef: resolveScriptRef({ code: alwaysSucceedCbor, version: "V3" }), + }, + }, + { + input: { + txHash: txHash("tx4"), + outputIndex: 0, + }, + output: { + address: serializePlutusScript({ + code: alwaysSucceedCbor, + version: "V3", + }).address, + amount: [ + { + unit: "lovelace", + quantity: "100000000", + }, + ], + }, + }, + ]); + + const txBuilder = new MeshTxBuilder({ + fetcher: offlineFetcher, + }); + + beforeEach(() => { + txBuilder.reset(); + }); + + it("should be able to add collateral return with tokens", async () => { + const txHex = await txBuilder + .spendingPlutusScriptV3() + .txIn(txHash("tx2"), 0) + .txInInlineDatumPresent() + .txInRedeemerValue("") + .txInScript(alwaysSucceedCbor) + .txInCollateral(txHash("tx1"), 0) + .changeAddress( + "addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9", + ) + .setTotalCollateral("5000000") + .complete(); + + const cardanoTx = Serialization.Transaction.fromCbor( + Serialization.TxCBOR(txHex), + ); + + expect(cardanoTx.body().totalCollateral()).toBe(5000000n); + expect(cardanoTx.body().collateralReturn()?.address().toBech32()).toBe( + "addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9", + ); + const collateralReturnValue = cardanoTx.body().collateralReturn()?.amount(); + expect( + empty( + subValue( + collateralReturnValue!, + Serialization.Value.fromCore({ + coins: 95000000n, + assets: new Map([ + [AssetId(mockTokenUnit(0)), 1000000n], + [AssetId(mockTokenUnit(1)), 10000n], + ]), + }), + ), + ), + ).toBe(true); + }); +}); diff --git a/packages/mesh-transaction/test/mesh-tx-builder/complete.test.ts b/packages/mesh-transaction/test/mesh-tx-builder/complete.test.ts index 523aa67c..b0a9a732 100644 --- a/packages/mesh-transaction/test/mesh-tx-builder/complete.test.ts +++ b/packages/mesh-transaction/test/mesh-tx-builder/complete.test.ts @@ -1,9 +1,7 @@ import { AccountInfo, - Action, emptyTxBuilderBody, IFetcher, - MintItem, MintParam, ScriptSource, TransactionInfo, @@ -12,6 +10,7 @@ import { UTxO, } from "@meshsdk/common"; import { MeshTxBuilder } from "@meshsdk/transaction"; +import {txHash} from "../test-util"; class MockTxBuilder extends MeshTxBuilder { constructor() { @@ -48,6 +47,9 @@ describe("MeshTxBuilder", () => { jest .spyOn(txBuilder.serializer as any, "serializeTxBody") .mockImplementation(() => ""); + jest + .spyOn(txBuilder.serializer as any, "serializeTxBodyWithMockSignatures") + .mockImplementation(() => ""); jest .spyOn(txBuilder as any, "addUtxosFromSelection") .mockImplementation(() => {}); @@ -62,7 +64,7 @@ describe("MeshTxBuilder", () => { newInputs: new Set(), newOutputs: new Set(), change: [], - fee: 100, + fee: 155381, redeemers: null, }; }); @@ -76,8 +78,8 @@ describe("MeshTxBuilder", () => { // Define incomplete inputs and mints const incompleteTxIns: TxIn[] = [ - { type: "PubKey", txIn: { txHash: "txHash1", txIndex: 0 } }, - { type: "PubKey", txIn: { txHash: "txHash2", txIndex: 1 } }, + { type: "PubKey", txIn: { txHash: txHash("tx1"), txIndex: 0 } }, + { type: "PubKey", txIn: { txHash: txHash("tx2"), txIndex: 1 } }, ]; const incompleteMints: MintParam[] = [ { @@ -91,7 +93,7 @@ describe("MeshTxBuilder", () => { ], scriptSource: { type: "Inline", - txHash: "txHash3", + txHash: txHash("tx3"), txIndex: 2, } as ScriptSource, }, diff --git a/packages/mesh-transaction/test/mesh-tx-builder/script-tx.test.ts b/packages/mesh-transaction/test/mesh-tx-builder/script-tx.test.ts index f5193205..22d3f68b 100644 --- a/packages/mesh-transaction/test/mesh-tx-builder/script-tx.test.ts +++ b/packages/mesh-transaction/test/mesh-tx-builder/script-tx.test.ts @@ -135,6 +135,7 @@ describe("MeshTxBuilder - Script Transactions", () => { "addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9", ) .setFee("5000000") + .setTotalCollateral("5000000") .complete(); expect( @@ -169,6 +170,13 @@ describe("MeshTxBuilder - Script Transactions", () => { ); expect(cardanoTx.body().fee().toString()).toBe("5000000"); expect(cardanoTx2.body().fee().toString()).toBe("5000000"); + expect(cardanoTx2.body().totalCollateral()).toBe(5000000n); + expect(cardanoTx2.body().collateralReturn()?.address().toBech32()).toBe( + "addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9", + ); + expect(cardanoTx2.body().collateralReturn()?.amount().coin()).toBe( + 95000000n, + ); }); it("should be able to spend from a script address with script ref", async () => { diff --git a/packages/mesh-transaction/test/mesh-tx-builder/vkeys-count.test.ts b/packages/mesh-transaction/test/mesh-tx-builder/vkeys-count.test.ts new file mode 100644 index 00000000..16e51a65 --- /dev/null +++ b/packages/mesh-transaction/test/mesh-tx-builder/vkeys-count.test.ts @@ -0,0 +1,58 @@ +import { MeshTxBuilderBody } from "@meshsdk/common"; +import { MeshTxBuilder } from "@meshsdk/transaction"; +import {baseAddress, drepBech32, keyHashHex, poolIdBech32, rewardAddress, txHash, vrfKeyHashHex} from "../test-util"; + +describe("MeshTxBuilder expected signatures", () => { + let txBuilder: MeshTxBuilder; + + beforeEach(() => { + txBuilder = new MeshTxBuilder(); + }); + + it("should have the correct number of possible vkeys for certificates", async () => { + const serializeTxBodyWithMockSignatures = jest + .spyOn(txBuilder.serializer as any, "serializeTxBodyWithMockSignatures"); + await txBuilder + .txIn( + txHash("tx3"), + 0, + [ + { + unit: "lovelace", + quantity: "3000000000000", + }, + ], + baseAddress(0), + 0, + ) + .drepRegistrationCertificate( + drepBech32(1), undefined, "100") + .drepDeregistrationCertificate(drepBech32(2), "100") + .drepUpdateCertificate(drepBech32(3)) + .delegateStakeCertificate(rewardAddress(4), poolIdBech32(5)) + .deregisterStakeCertificate(rewardAddress(6)) + .registerPoolCertificate({ + vrfKeyHash: vrfKeyHashHex(8), + operator: keyHashHex(7), + pledge: "400", + cost: "500", + margin: [1, 2], + relays: [{ + type: "MultiHostName", + domainName: "localhost", + }], + owners: [ + keyHashHex(9), + keyHashHex(10), + ], + rewardAddress: rewardAddress(11), + }) + .retirePoolCertificate(poolIdBech32(12), 13) + .registerStakeCertificate(rewardAddress(14)) + .changeAddress(baseAddress(15)) + .voteDelegationCertificate({alwaysAbstain: null}, rewardAddress(16)) + .complete() + const callArg = serializeTxBodyWithMockSignatures.mock.calls[0]![0] as MeshTxBuilderBody; + expect(callArg.expectedNumberKeyWitnesses).toEqual(12); + }); +}); diff --git a/packages/mesh-transaction/test/test-util.ts b/packages/mesh-transaction/test/test-util.ts index a89573a7..177e2df7 100644 --- a/packages/mesh-transaction/test/test-util.ts +++ b/packages/mesh-transaction/test/test-util.ts @@ -12,6 +12,7 @@ import { Transaction, TransactionOutput, TxCBOR, + HexBlob, } from "@meshsdk/core-cst"; export const txHash = (tx: string) => { @@ -47,11 +48,59 @@ export const baseAddress = (index: number) => { return encodeBech32Address(bytes); }; +export const drepBech32 = (index: number) => { + const numberString = index.toString().padStart(28, "0"); + const bytes = Buffer.concat([ + Buffer.from([0x22]), + Buffer.from(numberString, "utf-8"), + ]); + return encodeBech32DRep(bytes); +} + +export const rewardAddress = (index: number) => { + const numberString = index.toString().padStart(28, "0"); + const bytes = Buffer.concat([ + Buffer.from([0xE1]), + Buffer.from(numberString, "utf-8"), + ]); + return encodeBech32RewardAddress(bytes); +} + +export const keyHashHex = (index: number) => { + const numberString = index.toString().padStart(28, "0"); + return HexBlob.fromBytes(Buffer.from(numberString, "utf-8"),).toString(); +} + +export const vrfKeyHashHex = (index: number) => { + const numberString = index.toString().padStart(32, "0"); + return HexBlob.fromBytes(Buffer.from(numberString, "utf-8"),).toString(); +} + +export const poolIdBech32 = (index: number) => { + const numberString = index.toString().padStart(28, "0"); + return encodeBech32PoolId(Buffer.from(numberString, "utf-8")); +} + +const encodeBech32PoolId = (bytes: Uint8Array): string => { + const words = bech32.toWords(bytes); + return bech32.encode("pool", words, 200); +}; + const encodeBech32Address = (bytes: Uint8Array): string => { const words = bech32.toWords(bytes); return bech32.encode("addr_test", words, 200); }; +const encodeBech32RewardAddress = (bytes: Uint8Array): string => { + const words = bech32.toWords(bytes); + return bech32.encode("stake", words, 200); +}; + +const encodeBech32DRep = (bytes: Uint8Array) => { + const words = bech32.toWords(bytes); + return bech32.encode("drep", words, 200); +} + export const mockTokenUnit = (num: number) => { const policyId = num.toString(16).padStart(56, "0"); const tokenName = num.toString(16).padStart(8, "0"); diff --git a/packages/mesh-wallet/package.json b/packages/mesh-wallet/package.json index 0315c660..01a00df2 100644 --- a/packages/mesh-wallet/package.json +++ b/packages/mesh-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@meshsdk/wallet", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "description": "Wallets - https://meshjs.dev/apis/wallets", "main": "./dist/index.cjs", "browser": "./dist/index.js", @@ -35,9 +35,9 @@ "typescript": "^5.3.3" }, "dependencies": { - "@meshsdk/common": "1.9.0-beta-39", - "@meshsdk/core-cst": "1.9.0-beta-39", - "@meshsdk/transaction": "1.9.0-beta-39", + "@meshsdk/common": "1.9.0-beta-40", + "@meshsdk/core-cst": "1.9.0-beta-40", + "@meshsdk/transaction": "1.9.0-beta-40", "@simplewebauthn/browser": "^13.0.0" }, "prettier": "@meshsdk/configs/prettier", diff --git a/scripts/mesh-cli/package.json b/scripts/mesh-cli/package.json index 01d6bde7..d3b494a1 100644 --- a/scripts/mesh-cli/package.json +++ b/scripts/mesh-cli/package.json @@ -3,7 +3,7 @@ "description": "A quick and easy way to bootstrap your Web3 app using Mesh.", "homepage": "https://meshjs.dev", "author": "MeshJS", - "version": "1.9.0-beta-39", + "version": "1.9.0-beta-40", "license": "Apache-2.0", "type": "module", "main": "./dist/index.cjs",