Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/tidy-executions-keep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fleet-sdk/mock-chain": patch
---

Fix `MockChain.execute()` mutating the caller's transaction object: outputs were distributed to parties by splicing the transaction's own `outputs` array, so a transaction passed as an EIP-12 object lost every output that matched a party (typically all of them). Outputs are now distributed over a copy, leaving the caller's object unchanged.
46 changes: 46 additions & 0 deletions packages/mock-chain/src/mockChain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,52 @@ describe("Contract execution and chain mocking", () => {
expect(() => chain.execute(unsignedTransaction)).not.to.throw();
});

it("Should not mutate the caller's transaction object on execution", () => {
const chain = new MockChain();

const bob = chain.newParty().withBalance({
nanoergs: 1000000000n,
tokens: [{ tokenId: SIGUSD_TOKEN_ID, amount: 1000n }]
});
const alice = chain.newParty();

const unsignedTransaction = new TransactionBuilder(38479)
.from(bob.utxos.toArray())
.to(
new OutputBuilder(SAFE_MIN_BOX_VALUE, alice.address).addTokens({
tokenId: SIGUSD_TOKEN_ID,
amount: 100n
})
)
.sendChangeTo(bob.address)
.payMinFee()
.build()
.toEIP12Object();

const outputsBefore = [...unsignedTransaction.outputs];
const inputsBefore = [...unsignedTransaction.inputs];

expect(chain.execute(unsignedTransaction)).to.be.true;

// the caller's object must be left exactly as it was passed in
expect(unsignedTransaction.outputs).to.have.length(outputsBefore.length);
expect(unsignedTransaction.outputs).to.deep.equal(outputsBefore);
for (let i = 0; i < outputsBefore.length; i++) {
expect(unsignedTransaction.outputs[i]).to.be.equal(outputsBefore[i]);
}
expect(unsignedTransaction.inputs).to.deep.equal(inputsBefore);

// outputs must still be delivered to the parties
expect(alice.balance).to.be.deep.equal({
nanoergs: SAFE_MIN_BOX_VALUE,
tokens: [{ tokenId: SIGUSD_TOKEN_ID, amount: 100n }]
});
expect(bob.balance).to.be.deep.equal({
nanoergs: 1000000000n - RECOMMENDED_MIN_FEE_VALUE - SAFE_MIN_BOX_VALUE,
tokens: [{ tokenId: SIGUSD_TOKEN_ID, amount: 900n }]
});
});

it("Should execute burning transaction", () => {
const chain = new MockChain();

Expand Down
5 changes: 4 additions & 1 deletion packages/mock-chain/src/mockChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ export class MockChain {

this.#pushMetadata(txObject);

const { inputs, outputs } = txObject;
const { inputs } = txObject;
// distribute outputs over a copy: splicing the transaction's own array
// would mutate the caller's transaction object
const outputs = [...txObject.outputs];
for (const party of this.#parties) {
for (let i = inputs.length - 1; i >= 0; i--) {
if (party.utxos.exists(inputs[i].boxId)) {
Expand Down