Skip to content

Commit

Permalink
feat(core): add plugins support
Browse files Browse the repository at this point in the history
  • Loading branch information
capt-nemo429 committed Dec 12, 2022
1 parent 8d0ebcd commit 439e737
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 17 deletions.
123 changes: 123 additions & 0 deletions packages/core/src/builder/pluginContext.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { NotAllowedTokenBurning } from "../errors";
import { regularBoxesMock } from "../tests/mocks/mockBoxes";
import { OutputBuilder } from "./outputBuilder";
import { createPluginContext, FleetPluginContext } from "./pluginContext";
import { TransactionBuilder } from "./transactionBuilder";

describe("Plugin context", () => {
const creationHeight = 894169;
let builder!: TransactionBuilder;
let context!: FleetPluginContext;

beforeEach(() => {
builder = new TransactionBuilder(creationHeight);
context = createPluginContext(builder);
});

it("Should add inputs", () => {
const fromMethod = jest.spyOn(builder, "from");
const configureSelectorMethod = jest.spyOn(builder, "configureSelector");

const newLen = context.addInputs(regularBoxesMock);

expect(fromMethod).toBeCalledTimes(1);
expect(configureSelectorMethod).toBeCalledTimes(1);
expect(builder.inputs).toHaveLength(newLen);
expect(builder.inputs).toHaveLength(regularBoxesMock.length);
});

it("Should add a single input", () => {
const fromMethod = jest.spyOn(builder, "from");
const configureSelectorMethod = jest.spyOn(builder, "configureSelector");

let newLen = context.addInputs(regularBoxesMock[0]);
expect(fromMethod).toBeCalledTimes(1);
expect(configureSelectorMethod).toBeCalledTimes(1);
expect(builder.inputs).toHaveLength(newLen);
expect(builder.inputs).toHaveLength(1);

newLen = context.addInputs(regularBoxesMock[1]);
expect(fromMethod).toBeCalledTimes(2);
expect(configureSelectorMethod).toBeCalledTimes(2);
expect(builder.inputs).toHaveLength(newLen);
expect(builder.inputs).toHaveLength(2);
});

it("Should add data inputs", () => {
const withDataFromMethod = jest.spyOn(builder, "withDataFrom");

const newLen = context.addDataInputs(regularBoxesMock);

expect(withDataFromMethod).toBeCalledTimes(1);
expect(builder.dataInputs).toHaveLength(newLen);
expect(builder.dataInputs).toHaveLength(regularBoxesMock.length);
});

it("Should add outputs", () => {
const toMethod = jest.spyOn(builder, "to");

let newLen = context.addOutputs(
new OutputBuilder(10000n, "9gn5Jo6T7m4pAzCdD9JFdRMPxnfKLPgcX68rD8RQvPLyJsTpKcq")
);

expect(toMethod).toBeCalledTimes(1);
expect(newLen).toBe(builder.outputs.length);

newLen = context.addOutputs(
new OutputBuilder(20000n, "9gn5Jo6T7m4pAzCdD9JFdRMPxnfKLPgcX68rD8RQvPLyJsTpKcq")
);

expect(toMethod).toBeCalledTimes(2);
expect(builder.outputs).toHaveLength(newLen);
expect(builder.outputs).toHaveLength(2);
});

it("Should burn tokens, plugin context allowed", () => {
const burnTokensMethod = jest.spyOn(builder, "burnTokens");

builder
.from(regularBoxesMock)
.configure((settings) => settings.allowTokenBurningFromPlugins(true));

context.burnTokens([
{ tokenId: "bf2afb01fde7e373e22f24032434a7b883913bd87a23b62ee8b43eba53c9f6c2", amount: 1n },
{ tokenId: "007fd64d1ee54d78dd269c8930a38286caa28d3f29d27cadcb796418ab15c283", amount: 126n }
]);

expect(burnTokensMethod).toBeCalledTimes(1);
expect(builder.burning).toHaveLength(2);
});

it("Should burn tokens, globally allowed", () => {
const burnTokensMethod = jest.spyOn(builder, "burnTokens");

builder.from(regularBoxesMock).configure((settings) => settings.allowTokenBurning(true));

context.burnTokens([
{ tokenId: "bf2afb01fde7e373e22f24032434a7b883913bd87a23b62ee8b43eba53c9f6c2", amount: 1n },
{ tokenId: "007fd64d1ee54d78dd269c8930a38286caa28d3f29d27cadcb796418ab15c283", amount: 126n }
]);

expect(burnTokensMethod).toBeCalledTimes(1);
expect(builder.burning).toHaveLength(2);
});

it("Should fail if token burning is not allowed", () => {
const burnTokensMethod = jest.spyOn(builder, "burnTokens");

builder.from(regularBoxesMock);

expect(() => {
context.burnTokens([
{ tokenId: "bf2afb01fde7e373e22f24032434a7b883913bd87a23b62ee8b43eba53c9f6c2", amount: 1n },
{
tokenId: "007fd64d1ee54d78dd269c8930a38286caa28d3f29d27cadcb796418ab15c283",
amount: 126n
}
]);
}).toThrow(NotAllowedTokenBurning);

expect(burnTokensMethod).not.toBeCalled();
expect(builder.burning).toBeUndefined();
});
});
58 changes: 58 additions & 0 deletions packages/core/src/builder/pluginContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Amount, Box, OneOrMore, TokenAmount } from "@fleet-sdk/common";
import { NotAllowedTokenBurning, OutputBuilder, TransactionBuilder } from "..";
import { CollectionAddOptions } from "../models/collections/collection";

export type FleetPluginContext = {
/**
* Add and ensures selection of one or more inputs to the inputs list
* @param inputs
* @returns new list length
*/
addInputs: (inputs: OneOrMore<Box<Amount>>, options?: CollectionAddOptions) => number;

/**
* Add one or more data inputs to the data inputs list
* @param dataInputs
* @returns new list length
*/
addDataInputs: (dataInputs: OneOrMore<Box<Amount>>, options?: CollectionAddOptions) => number;

/**
* Add one or more outputs to the outputs list
* @param outputs
* @param options
* @returns new list length
*/
addOutputs: (outputs: OneOrMore<OutputBuilder>, options?: CollectionAddOptions) => number;

/**
* Burn tokens
* @param tokens
* @throws Burning tokens thought a plugin, requires explicitly permission
* from {@link TransactionBuilder.configure}, if token burning is not allowed
* it will thrown a {@link NotAllowedTokenBurning} exception.
*/
burnTokens: (tokens: OneOrMore<TokenAmount<Amount>>) => void;
};

export function createPluginContext(transactionBuilder: TransactionBuilder): FleetPluginContext {
return {
addInputs: (inputs, options) =>
transactionBuilder
.from(inputs, options)
.configureSelector((selector) =>
selector.ensureInclusion(
Array.isArray(inputs) ? inputs.map((input) => input.boxId) : inputs.boxId
)
).inputs.length,
addOutputs: (outputs, options) => transactionBuilder.to(outputs, options).outputs.length,
addDataInputs: (dataInputs, options) =>
transactionBuilder.withDataFrom(dataInputs, options).dataInputs.length,
burnTokens: (tokens) => {
if (!transactionBuilder.settings.canBurnTokensFromPlugins) {
throw new NotAllowedTokenBurning();
}
transactionBuilder.burnTokens(tokens);
}
};
}
127 changes: 127 additions & 0 deletions packages/core/src/builder/transactionBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,3 +1061,130 @@ describe("Token burning", () => {
}).toThrow(MalformedTransaction);
});
});

describe("Plugins", () => {
it("Should include inputs, data inputs and outputs from plugin", () => {
const tx = new TransactionBuilder(height)
.extend(({ addInputs, addDataInputs, addOutputs }) => {
addInputs(regularBoxesMock);
addDataInputs(first(regularBoxesMock));
addOutputs(new OutputBuilder(SAFE_MIN_BOX_VALUE, a1.address, height));
})
.sendChangeTo("9hY16vzHmmfyVBwKeFGHvb2bMFsG94A1u7To1QWtUokACyFVENQ")
.build();

expect(tx.outputs).toHaveLength(2); // output from plugin context and change
expect(tx.outputs[0].ergoTree).toBe(a1.ergoTree);
expect(tx.inputs).toHaveLength(regularBoxesMock.length); // should include all inputs added from plugin context
expect(tx.dataInputs).toHaveLength(1);
expect(tx.dataInputs[0].boxId).toBe(regularBoxesMock[0].boxId);
});

it("Should include inputs, data inputs and outputs from multiple plugins", () => {
const tx = new TransactionBuilder(height)
.extend(({ addInputs, addDataInputs, addOutputs }) => {
addInputs(regularBoxesMock[1]);
addDataInputs(regularBoxesMock[1]);
addOutputs(new OutputBuilder(SAFE_MIN_BOX_VALUE, a1.address, height));
})
.extend(({ addInputs, addDataInputs, addOutputs }) => {
addInputs(regularBoxesMock[2]);
addDataInputs(regularBoxesMock[2]);
addOutputs(new OutputBuilder(SAFE_MIN_BOX_VALUE, a2.address, height));
})
.sendChangeTo("9hY16vzHmmfyVBwKeFGHvb2bMFsG94A1u7To1QWtUokACyFVENQ")
.build();

expect(tx.outputs).toHaveLength(3); // two output from plugin context and one from change
expect(tx.outputs[0].ergoTree).toBe(a1.ergoTree);
expect(tx.outputs[1].ergoTree).toBe(a2.ergoTree);

expect(tx.inputs).toHaveLength(2); // should include all inputs added from plugin context

expect(tx.dataInputs).toHaveLength(2);
expect(tx.dataInputs[0].boxId).toBe(regularBoxesMock[1].boxId);
expect(tx.dataInputs[1].boxId).toBe(regularBoxesMock[2].boxId);
});

it("Should burn tokens, plugin context allowed", () => {
const tokenIda = "bf2afb01fde7e373e22f24032434a7b883913bd87a23b62ee8b43eba53c9f6c2";
const tokenIdb = "007fd64d1ee54d78dd269c8930a38286caa28d3f29d27cadcb796418ab15c283";

const tx = new TransactionBuilder(height)
.from(regularBoxesMock)
.configure((settings) => settings.allowTokenBurningFromPlugins(true))
.extend(({ burnTokens }) => {
burnTokens([
{ tokenId: tokenIda, amount: 1n },
{ tokenId: tokenIdb, amount: 126n }
]);
})
.sendChangeTo("9hY16vzHmmfyVBwKeFGHvb2bMFsG94A1u7To1QWtUokACyFVENQ")
.build("EIP-12");

expect(utxoSum(tx.outputs, tokenIda) - utxoSum(tx.inputs, tokenIda)).toBe(-1n);
expect(utxoSum(tx.outputs, tokenIdb) - utxoSum(tx.inputs, tokenIdb)).toBe(-126n);
});

it("Should burn tokens, global context allowed", () => {
const tokenIda = "bf2afb01fde7e373e22f24032434a7b883913bd87a23b62ee8b43eba53c9f6c2";
const tokenIdb = "007fd64d1ee54d78dd269c8930a38286caa28d3f29d27cadcb796418ab15c283";

const tx = new TransactionBuilder(height)
.from(regularBoxesMock)
.configure((settings) => settings.allowTokenBurning(true))
.extend(({ burnTokens }) => {
burnTokens([
{ tokenId: tokenIda, amount: 1n },
{ tokenId: tokenIdb, amount: 126n }
]);
})
.sendChangeTo("9hY16vzHmmfyVBwKeFGHvb2bMFsG94A1u7To1QWtUokACyFVENQ")
.build("EIP-12");

expect(utxoSum(tx.outputs, tokenIda) - utxoSum(tx.inputs, tokenIda)).toBe(-1n);
expect(utxoSum(tx.outputs, tokenIdb) - utxoSum(tx.inputs, tokenIdb)).toBe(-126n);
});

it("Should fail if burning is not allowed", () => {
const tokenIda = "bf2afb01fde7e373e22f24032434a7b883913bd87a23b62ee8b43eba53c9f6c2";
const tokenIdb = "007fd64d1ee54d78dd269c8930a38286caa28d3f29d27cadcb796418ab15c283";

expect(() => {
new TransactionBuilder(height)
.from(regularBoxesMock)
.extend(({ burnTokens }) => {
burnTokens([
{ tokenId: tokenIda, amount: 1n },
{ tokenId: tokenIdb, amount: 126n }
]);
})
.build();
}).toThrow(NotAllowedTokenBurning);
});

it("Should not execute plugins more tha one time", () => {
const plugin = jest.fn(({ addOutputs }) => {
addOutputs(new OutputBuilder(SAFE_MIN_BOX_VALUE, a1.address));
});

const tx = new TransactionBuilder(height)
.from(regularBoxesMock)
.extend(plugin)
.sendChangeTo(a2.address);

let builtTx = tx.build();

expect(tx.outputs).toHaveLength(1);
expect(builtTx.outputs).toHaveLength(2); // output from plugin and change

expect(tx.outputs.at(0).ergoTree).toBe(a1.ergoTree);
expect(plugin).toBeCalledTimes(1);

for (let i = 0; i < 5; i++) {
builtTx = tx.build();
}

expect(plugin).toBeCalledTimes(1); // should not do subsequent calls
});
});
Loading

0 comments on commit 439e737

Please sign in to comment.