import "reflect-metadata"; import { Balance, Balances, BalancesKey, TokenId } from "@proto-kit/library"; import { runtimeMethod, runtimeModule, RuntimeModule } from "@proto-kit/module"; import { PrivateKey } from "o1js"; import { inject } from "tsyringe"; import { expectDefined } from "@proto-kit/common"; import { TestingAppChain } from "../src"; @runtimeModule() class Faucet extends RuntimeModule { public constructor(@inject("Balances") public balances: Balances) { super(); } @runtimeMethod() public async drip() { await this.balances.mint( TokenId.from(0), this.transaction.sender.value, Balance.from(1000) ); } } @runtimeModule() class Pit extends RuntimeModule { public constructor(@inject("Balances") public balances: Balances) { super(); } @runtimeMethod() public async burn(amount: Balance) { await this.balances.burn( TokenId.from(0), this.transaction.sender.value, amount ); } } describe("fees", () => { const feeRecipientKey = PrivateKey.random(); const senderKey = PrivateKey.random(); const appChain = TestingAppChain.fromRuntime({ Faucet, Pit, }); beforeAll(async () => { appChain.configurePartial({ Runtime: { Faucet: {}, Pit: {}, Balances: {}, }, Protocol: { ...appChain.config.Protocol!, TransactionFee: { tokenId: 0n, feeRecipient: feeRecipientKey.toPublicKey().toBase58(), baseFee: 0n, perWeightUnitFee: 1n, methods: { "Faucet.drip": { baseFee: 0n, weight: 0n, perWeightUnitFee: 0n, }, }, }, }, }); await appChain.start(); appChain.setSigner(senderKey); }, 60_000); afterAll(async () => { await appChain.close(); }); it("should allow a free faucet transaction", async () => { expect.assertions(2); const faucet = appChain.runtime.resolve("Faucet"); const tx = await appChain.transaction(senderKey.toPublicKey(), async () => { await faucet.drip(); }); await tx.sign(); await tx.send(); await appChain.produceBlock(); const balance = await appChain.query.runtime.Balances.balances.get( new BalancesKey({ tokenId: new TokenId(0), address: senderKey.toPublicKey(), }) ); expectDefined(balance); expect(balance.toString()).toBe("1000"); }); it("should allow burning of tokens with a fixed fee", async () => { expect.assertions(7); const pit = appChain.runtime.resolve("Pit"); const transactionFeeModule = appChain.protocol.resolve("TransactionFee"); const balanceSenderBefore = await appChain.query.runtime.Balances.balances.get( new BalancesKey({ tokenId: new TokenId(0), address: senderKey.toPublicKey(), }) ); const balanceFeeReceiverBefore = await appChain.query.runtime.Balances.balances.get( new BalancesKey({ tokenId: new TokenId(0), address: feeRecipientKey.toPublicKey(), }) ); expectDefined(balanceFeeReceiverBefore); expect(balanceFeeReceiverBefore.toString()).toBe("0"); const burnAmount = Balance.from(100); const senderBalanceBefore = balanceSenderBefore; const tx = await appChain.transaction(senderKey.toPublicKey(), async () => { await pit.burn(burnAmount); }); const methodId = tx.transaction?.methodId.toBigInt(); expectDefined(methodId); const transactionFeeConfig = transactionFeeModule.feeAnalyzer.getFeeConfig(methodId); const transactionFee = transactionFeeModule.getFee(transactionFeeConfig); await tx.sign(); await tx.send(); await appChain.produceBlock(); const balanceSenderAfter = await appChain.query.runtime.Balances.balances.get( new BalancesKey({ tokenId: new TokenId(0), address: senderKey.toPublicKey(), }) ); const balanceFeeReceiverAfter = await appChain.query.runtime.Balances.balances.get( new BalancesKey({ tokenId: new TokenId(0), address: feeRecipientKey.toPublicKey(), }) ); const expectedSenderBalanceAfter = senderBalanceBefore! .sub(burnAmount) .sub(transactionFee); expectDefined(balanceSenderAfter); expect(balanceSenderAfter.toString()).toBe( expectedSenderBalanceAfter.toString() ); expectDefined(balanceFeeReceiverAfter); expect(balanceFeeReceiverAfter.toString()).toBe(transactionFee.toString()); }); });