FeeProxy (Native API example)
Pay gas in a token other than XRP, around a remark, a batch, an EVM call and a FuturePass proxy call. Reference: feeProxy pallet.
Copied from Futureverse’s
futureversecom/trn-examples, deleted with its GitHub organisation, at commit44c9e16of 2024-10-31. Apache-2.0: license. Before you run it, read Running the examples.
You don’t need a FuturePass
This page treats FuturePass as something to use. You don’t need one: where the page uses a FuturePass, use an account in your own wallet instead — any EIP-6963 wallet app, MetaMask preferred. If you have a FuturePass, you can move what’s in it to your wallet at any time: https://futurepass.trn.codeology.co.nz/wallet/futurepass. FuturePass explains how it works.
Offline destinations
Some places this page points to did not answer when last checked:
docs-beta.therootnetwork.com— does not resolve (checked 2026-10-18)stackblitz.com/github/futureversecom— StackBlitz loads these projects from the deleted futureversecom organisation on GitHub (checked 2026-10-18)
Run in StackBlitz (repository deleted, checked 2026-10-18) · Pallet Documentation (offline, checked 2026-10-18)
IMPORTANT
Ensure the following ENV vars are available before running the examples
CALLER_PRIVATE_KEY- Private key of an account that submits the transaction. Follow this guide to create and fund an account with some test tokens on Porcini (testnet) if you don't have one yet.
Examples
bash
# change your working directory to this example first
cd examples/substrate/use-feeProxy
# export all required environments
export CALLER_PRIVATE_KEY=
# `feeProxy.callWithFeePreferences` that wraps around `system.remarkWithEvent`
pnpm call:callSystemRemark
# `feeProxy.callWithFeePreferences` that wraps around `utility.batchAll`
pnpm call:callBatchAll
# `feeProxy.callWithFeePreferences` that wraps around `evm.call`
pnpm call:callEVMCall
# `feeProxy.callWithFeePreferences` that wraps around `futurepass.proxyExtrinsic`
pnpm call:callProxyExtrinsic
# `feeProxy.callWithFeePreferences` that wraps around `futurepass.proxyExtrinsic` then `emv.call`
pnpm call:callProxyExtrinsicEVMCallSource files
src/callBatchAll.ts
ts
import { ALICE, BOB, CHARLIE } from "@trne/utils/accounts";
import { filterExtrinsicEvents } from "@trne/utils/filterExtrinsicEvents";
import { formatEventData } from "@trne/utils/formatEventData";
import { ASTO_ASSET_ID, XRP_ASSET_ID } from "@trne/utils/porcini-assets";
import { sendExtrinsic } from "@trne/utils/sendExtrinsic";
import { withChainApi } from "@trne/utils/withChainApi";
interface AmountsIn {
Ok: [number, number];
}
/**
* Use `feeProxy.callWithFeePreferences` to trigger `utility.batchAll` call, which contain multiple
* calls of `assets.trafer`, and pay gas in ASTO token.
*
* Assumes the caller has ASTO balance.
*/
withChainApi("porcini", async (api, caller, logger) => {
/**
* 1. Create `utility.batchAll` call that batches 3 transfer calls to ALICE, BOB & CHARLIE
*/
const oneASTO = 1 * Math.pow(10, 18); // 1 ASTO in `wei` unit
const transferToAliceCall = api.tx.assets.transfer(ASTO_ASSET_ID, ALICE, oneASTO.toString());
const transferToBobCall = api.tx.assets.transfer(ASTO_ASSET_ID, BOB, oneASTO.toString());
const transferToCharlieCall = api.tx.assets.transfer(ASTO_ASSET_ID, CHARLIE, oneASTO.toString());
logger.info(
{
parameters: [
transferToAliceCall.toJSON(),
transferToBobCall.toJSON(),
transferToCharlieCall.toJSON(),
],
},
`create a "system.remarkWithEvent"`
);
const batchAllCall = api.tx.utility.batchAll([
transferToAliceCall,
transferToBobCall,
transferToCharlieCall,
]);
/**
* 2. Determine the `maxPayment` in ASTO by estimate the gas cost and use `dex` to get a quote
*/
// we need a dummy feeProxy call (with maxPayment=0) to do a proper fee estimation
const feeProxyCallForEstimation = api.tx.feeProxy.callWithFeePreferences(
ASTO_ASSET_ID,
0,
batchAllCall
);
const paymentInfo = await feeProxyCallForEstimation.paymentInfo(caller.address);
const estimatedFee = paymentInfo.partialFee.toString();
// query the the `dex` to determine the `maxPayment` you are willing to pay
const {
Ok: [amountIn],
} = (await api.rpc.dex.getAmountsIn(estimatedFee, [
ASTO_ASSET_ID,
XRP_ASSET_ID,
])) as unknown as AmountsIn;
// allow a buffer to avoid slippage, 5%
const maxPayment = Number(amountIn * 1.05).toFixed();
/**
* 3. Create and dispatch `feeProxy.callWithFeePreferences` extrinsic
*/
logger.info(
{
parameters: {
paymentAsset: ASTO_ASSET_ID,
maxPayment,
call: batchAllCall.toJSON(),
},
},
`create a "feeProxy.callWithFeePreferences"`
);
const feeProxyCall = api.tx.feeProxy.callWithFeePreferences(
ASTO_ASSET_ID,
maxPayment,
batchAllCall
);
logger.info(`dispatch extrinsic from caller="${caller.address}"`);
const { result, extrinsicId } = await sendExtrinsic(feeProxyCall, caller, { log: logger });
const [proxyEvent, batchEvent, aliceTransferEvent, bobTransferEvent, charlieTransferEvent] =
filterExtrinsicEvents(result.events, [
"FeeProxy.CallWithFeePreferences",
"Utility.BatchCompleted",
{ name: "Assets.Transferred", key: "to", data: { value: ALICE, type: "T::AccountId" } },
{ name: "Assets.Transferred", key: "to", data: { value: BOB, type: "T::AccountId" } },
{ name: "Assets.Transferred", key: "to", data: { value: CHARLIE, type: "T::AccountId" } },
]);
logger.info(
{
result: {
extrinsicId,
blockNumber: result.blockNumber,
proxyEvent: formatEventData(proxyEvent.event),
batchEvent: formatEventData(batchEvent.event),
aliceTransferEvent: formatEventData(aliceTransferEvent.event),
bobTransferEvent: formatEventData(bobTransferEvent.event),
charlieTransferEvent: formatEventData(charlieTransferEvent.event),
},
},
"receive result"
);
});src/callEVMCall.ts
ts
import { ApiPromise } from "@polkadot/api";
import { ERC20_ABI } from "@therootnetwork/evm";
import { ALICE } from "@trne/utils/accounts";
import { filterExtrinsicEvents } from "@trne/utils/filterExtrinsicEvents";
import { formatEventData } from "@trne/utils/formatEventData";
import { getERC20Contract } from "@trne/utils/getERC20Contract";
import { getFeeProxyPricePair } from "@trne/utils/getFeeProxyPricePair";
import { Logger } from "@trne/utils/getLogger";
import { ASTO_ASSET_ID, SYLO_ASSET_ID, XRP_ASSET_ID } from "@trne/utils/porcini-assets";
import { sendExtrinsic } from "@trne/utils/sendExtrinsic";
import { withChainApi } from "@trne/utils/withChainApi";
import { provideEthersProvider } from "@trne/utils/withEthersProvider";
import { BigNumber, utils } from "ethers";
interface AmountsIn {
Ok: [number, number];
}
/**
* Use `feeProxy.callWithFeePreferencs` to trigger `evm.call`
*
* Assumes the caller has ASTO balance to pay for gas and SYLO balance to demonstrate
* the transfer
*/
withChainApi("porcini", async (api, caller, logger) => {
/**
* 1. Create `emv.call` call
*/
const { call: evmCall, estimateGasCost } = await createEVMCall(caller.address, api, logger);
/**
* 2. Determine the `maxPayment` in ASTO by estimate the gas cost and use `dex` to get a quote
*/
// we need a dummy feeProxy call (with maxPayment=0) to do a proper fee estimation
const feeProxyCallForEstimation = api.tx.feeProxy.callWithFeePreferences(
ASTO_ASSET_ID,
0,
evmCall
);
const paymentInfo = await feeProxyCallForEstimation.paymentInfo(caller.address);
// we need to add the actual estimate cost for the EVM layer call as part of the estimation
const estimatedFee = BigNumber.from(paymentInfo.partialFee.toString())
.add(estimateGasCost)
.toString();
// query the the `dex` to determine the `maxPayment` you are willing to pay
const {
Ok: [amountIn],
} = (await api.rpc.dex.getAmountsIn(estimatedFee, [
ASTO_ASSET_ID,
XRP_ASSET_ID,
])) as unknown as AmountsIn;
// allow a buffer to avoid slippage, 5%
const maxPayment = Number(amountIn * 1.05).toFixed();
/**
* 3. Create and dispatch `feeProxy.callWithFeePreferences` extrinsic
*/
logger.info(
{
parameters: {
paymentAsset: ASTO_ASSET_ID,
maxPayment,
call: evmCall.toJSON(),
},
},
`create a "feeProxy.callWithFeePreferences"`
);
const feeProxyCall = api.tx.feeProxy.callWithFeePreferences(ASTO_ASSET_ID, maxPayment, evmCall);
logger.info(`dispatch extrinsic from caller="${caller.address}"`);
const { result, extrinsicId } = await sendExtrinsic(feeProxyCall, caller, { log: logger });
const [proxyEvent, evmLogEvent, aliceTransferEvent] = filterExtrinsicEvents(result.events, [
"FeeProxy.CallWithFeePreferences",
"Evm.Log",
{ name: "Assets.Transferred", key: "to", data: { value: ALICE, type: "T::AccountId" } },
]);
logger.info(
{
result: {
extrinsicId,
blockNumber: result.blockNumber,
proxyEvent: formatEventData(proxyEvent.event),
evmLogEvent: formatEventData(evmLogEvent.event),
aliceTransferEvent: formatEventData(aliceTransferEvent.event),
},
},
"receive result"
);
});
async function createEVMCall(caller: string, api: ApiPromise, logger: Logger) {
// setup the actual EVM transaction, as interface
const { provider, wallet } = await provideEthersProvider("porcini");
const transferToken = getERC20Contract(SYLO_ASSET_ID).connect(wallet);
const transferAmount = utils.parseUnits("0.1", 18); // 0.1 SYLO
const transferInput = new utils.Interface(ERC20_ABI).encodeFunctionData("transfer", [
ALICE,
transferAmount,
]);
const transferEstimate = await transferToken.estimateGas.transfer(ALICE, transferAmount);
const { maxFeePerGas, estimateGasCost } = await getFeeProxyPricePair(
provider,
transferEstimate,
ASTO_ASSET_ID,
0.05
);
logger.info(
{
parameters: {
source: caller,
target: transferToken.address,
input: transferInput,
gasLimit: transferEstimate.toString(),
maxFeePerGas: maxFeePerGas.toString(),
},
},
`create an "emv.call"`
);
return {
call: api.tx.evm.call(
caller,
transferToken.address,
transferInput,
0,
transferEstimate.toString(),
maxFeePerGas.toString(),
0,
null,
[]
),
estimateGasCost,
};
}src/callProxyExtrinsic.ts
ts
import { filterExtrinsicEvents } from "@trne/utils/filterExtrinsicEvents";
import { formatEventData } from "@trne/utils/formatEventData";
import { ASTO_ASSET_ID, XRP_ASSET_ID } from "@trne/utils/porcini-assets";
import { sendExtrinsic } from "@trne/utils/sendExtrinsic";
import { withChainApi } from "@trne/utils/withChainApi";
import assert from "node:assert";
interface AmountsIn {
Ok: [number, number];
}
/**
* Use `feeProxy.callWithFeePreferences` to trigger `system.remarkWithEvent` call via
* `futurepass.proxyExtrinsic`, and have Futurepass account pays gas in ASTO.
*
* Assumes the caller has a valid Futurepass account and ASTO balance to pay for gas.
*/
withChainApi("porcini", async (api, caller, logger) => {
/**
* 1. Create `futurepass.proxyExtrinsic` call that wraps around `system.remarkWithEvent` call
*/
const fpAccount = (await api.query.futurepass.holders(caller.address)).unwrap();
logger.info(
{
futurepass: {
holder: caller.address,
account: fpAccount.toString(),
},
},
"futurepass details"
);
assert(fpAccount);
// can be any extrinsic, using `system.remarkWithEvent` for simplicity
const remarkCall = api.tx.system.remarkWithEvent("Hello World");
logger.info(
{
parameters: {
futurepass: fpAccount,
call: remarkCall.toJSON(),
},
},
`create a "futurepass.proxyExtrinsic"`
);
const futurepassCall = api.tx.futurepass.proxyExtrinsic(fpAccount, remarkCall);
/**
* 2. Determine the `maxPayment` in ASTO by estimate the gas cost and use `dex` to get a quote
*/
// we need a dummy feeProxy call (with maxPayment=0) to do a proper fee estimation
const feeProxyCallForEstimation = api.tx.feeProxy.callWithFeePreferences(
ASTO_ASSET_ID,
0,
futurepassCall
);
const paymentInfo = await feeProxyCallForEstimation.paymentInfo(caller.address);
const estimatedFee = paymentInfo.partialFee.toString();
// query the the `dex` to determine the `maxPayment` you are willing to pay
const {
Ok: [amountIn],
} = (await api.rpc.dex.getAmountsIn(estimatedFee, [
ASTO_ASSET_ID,
XRP_ASSET_ID,
])) as unknown as AmountsIn;
// allow a buffer to avoid slippage, 5%
const maxPayment = Number(amountIn * 1.05).toFixed();
/**
* 3. Create and dispatch `feeProxy.callWithFeePreferences` extrinsic
*/
logger.info(
{
parameters: {
paymentAsset: ASTO_ASSET_ID,
maxPayment,
call: futurepassCall.toJSON(),
},
},
`create a "feeProxy.callWithFeePreferences"`
);
const feeProxyCall = api.tx.feeProxy.callWithFeePreferences(
ASTO_ASSET_ID,
maxPayment,
futurepassCall
);
logger.info(`dispatch extrinsic from caller="${caller.address}"`);
const { result, extrinsicId } = await sendExtrinsic(feeProxyCall, caller, { log: logger });
const [proxyEvent, futurepassEvent, remarkEvent] = filterExtrinsicEvents(result.events, [
"FeeProxy.CallWithFeePreferences",
"Futurepass.ProxyExecuted",
"System.Remarked",
]);
logger.info(
{
result: {
extrinsicId,
blockNumber: result.blockNumber,
proxyEvent: formatEventData(proxyEvent.event),
futurepassEvent: formatEventData(futurepassEvent.event),
remarkEvent: formatEventData(remarkEvent.event),
},
},
"receive result"
);
});src/callProxyExtrinsicEVMCall.ts
ts
import { ApiPromise } from "@polkadot/api";
import { ERC20_ABI } from "@therootnetwork/evm";
import { ALICE } from "@trne/utils/accounts";
import { filterExtrinsicEvents } from "@trne/utils/filterExtrinsicEvents";
import { formatEventData } from "@trne/utils/formatEventData";
import { getERC20Contract } from "@trne/utils/getERC20Contract";
import { getFeeProxyPricePair } from "@trne/utils/getFeeProxyPricePair";
import { Logger } from "@trne/utils/getLogger";
import { ASTO_ASSET_ID, SYLO_ASSET_ID, XRP_ASSET_ID } from "@trne/utils/porcini-assets";
import { sendExtrinsic } from "@trne/utils/sendExtrinsic";
import { withChainApi } from "@trne/utils/withChainApi";
import { provideEthersProvider } from "@trne/utils/withEthersProvider";
import { BigNumber, utils } from "ethers";
import assert from "node:assert";
interface AmountsIn {
Ok: [number, number];
}
/**
* Use `feeProxy.callWithFeePreferences` to trigger `emv.call` call via
* `futurepass.proxyExtrinsic`, and have Futurepass account pays gas in ASTO.
*
* Assumes the caller has a valid Futurepass account, ASTO balance to pay for gas and
* SYLO balance to demonstrate the transfer
*/
withChainApi("porcini", async (api, caller, logger) => {
/**
* 1. Create `futurepass.proxyExtrinsic` call that wraps around `evm.call`
*/
const fpAccount = (await api.query.futurepass.holders(caller.address)).unwrapOr(undefined);
assert(fpAccount);
logger.info(
{
futurepass: {
holder: caller.address,
account: fpAccount.toString(),
},
},
"futurepass details"
);
const { call: evmCall, estimateGasCost } = await createEVMCall(fpAccount.toString(), api, logger);
logger.info(
{
parameters: {
futurepass: fpAccount,
call: evmCall.toJSON(),
},
},
`create a "futurepass.proxyExtrinsic"`
);
const futurepassCall = api.tx.futurepass.proxyExtrinsic(fpAccount, evmCall);
/**
* 2. Determine the `maxPayment` in ASTO by estimate the gas cost and use `dex` to get a quote
*/
// we need a dummy feeProxy call (with maxPayment=0) to do a proper fee estimation
const feeProxyCallForEstimation = api.tx.feeProxy.callWithFeePreferences(
ASTO_ASSET_ID,
0,
futurepassCall
);
const paymentInfo = await feeProxyCallForEstimation.paymentInfo(caller.address);
// we need to add the actual estimate cost for the EVM layer call as part of the estimation
const estimatedFee = BigNumber.from(paymentInfo.partialFee.toString())
.add(estimateGasCost)
.toString();
// query the the `dex` to determine the `maxPayment` you are willing to pay
const {
Ok: [amountIn],
} = (await api.rpc.dex.getAmountsIn(estimatedFee, [
ASTO_ASSET_ID,
XRP_ASSET_ID,
])) as unknown as AmountsIn;
// allow a buffer to avoid slippage, 5%
const maxPayment = Number(amountIn * 1.05).toFixed();
/**
* 3. Create and dispatch `feeProxy.callWithFeePreferences` extrinsic
*/
logger.info(
{
parameters: {
paymentAsset: ASTO_ASSET_ID,
maxPayment,
call: futurepassCall.toJSON(),
},
},
`create a "feeProxy.callWithFeePreferences"`
);
const feeProxyCall = api.tx.feeProxy.callWithFeePreferences(
ASTO_ASSET_ID,
maxPayment,
futurepassCall
);
logger.info(`dispatch extrinsic from caller="${caller.address}"`);
const { result, extrinsicId } = await sendExtrinsic(feeProxyCall, caller, { log: logger });
const [proxyEvent, futurepassEvent, evmLogEvent, aliceTransferEvent] = filterExtrinsicEvents(
result.events,
[
"FeeProxy.CallWithFeePreferences",
"Futurepass.ProxyExecuted",
"Evm.Log",
{ name: "Assets.Transferred", key: "to", data: { value: ALICE, type: "T::AccountId" } },
]
);
logger.info(
{
result: {
extrinsicId,
blockNumber: result.blockNumber,
proxyEvent: formatEventData(proxyEvent.event),
futurepassEvent: formatEventData(futurepassEvent.event),
evmLogEvent: formatEventData(evmLogEvent.event),
aliceTransferEvent: formatEventData(aliceTransferEvent.event),
},
},
"receive result"
);
});
async function createEVMCall(caller: string, api: ApiPromise, logger: Logger) {
// setup the actual EVM transaction, as interface
const { provider, wallet } = await provideEthersProvider("porcini");
const transferToken = getERC20Contract(SYLO_ASSET_ID).connect(wallet);
const transferAmount = utils.parseUnits("0.1", 18); // 0.1 SYLO
const transferInput = new utils.Interface(ERC20_ABI).encodeFunctionData("transfer", [
ALICE,
transferAmount,
]);
const transferEstimate = await transferToken.estimateGas.transfer(ALICE, transferAmount);
const { maxFeePerGas, estimateGasCost } = await getFeeProxyPricePair(
provider,
transferEstimate,
ASTO_ASSET_ID,
0.05
);
logger.info(
{
parameters: {
source: caller,
target: transferToken.address,
input: transferInput,
gasLimit: transferEstimate.toString(),
maxFeePerGas: maxFeePerGas.toString(),
},
},
`create an "emv.call"`
);
return {
call: api.tx.evm.call(
caller,
transferToken.address,
transferInput,
0,
transferEstimate.toString(),
maxFeePerGas.toString(),
0,
null,
[]
),
estimateGasCost,
};
}src/callSystemRemark.ts
ts
import { filterExtrinsicEvents } from "@trne/utils/filterExtrinsicEvents";
import { formatEventData } from "@trne/utils/formatEventData";
import { ASTO_ASSET_ID, XRP_ASSET_ID } from "@trne/utils/porcini-assets";
import { sendExtrinsic } from "@trne/utils/sendExtrinsic";
import { withChainApi } from "@trne/utils/withChainApi";
interface AmountsIn {
Ok: [number, number];
}
/**
* Use `feeProxy.callWithFeePreferences` to trigger `system.remarkWithEvent` call, and pay gas
* in ASTO token.
*
* Assumes the caller has ASTO balance.
*/
withChainApi("porcini", async (api, caller, logger) => {
/**
* 1. Create `system.remarkWithEvent` call
*/
// can be any extrinsic, using `system.remarkWithEvent` for simplicity
logger.info(
{
parameters: {
remark: "Hello World",
},
},
`create a "system.remarkWithEvent"`
);
const remarkCall = api.tx.system.remarkWithEvent("Hello World");
/**
* 2. Determine the `maxPayment` in ASTO by estimate the gas cost and use `dex` to get a quote
*/
// we need a dummy feeProxy call (with maxPayment=0) to do a proper fee estimation
const feeProxyCallForEstimation = api.tx.feeProxy.callWithFeePreferences(
ASTO_ASSET_ID,
0,
remarkCall
);
const paymentInfo = await feeProxyCallForEstimation.paymentInfo(caller.address);
const estimatedFee = paymentInfo.partialFee.toString();
// query the the `dex` to determine the `maxPayment` you are willing to pay
const {
Ok: [amountIn],
} = (await api.rpc.dex.getAmountsIn(estimatedFee, [
ASTO_ASSET_ID,
XRP_ASSET_ID,
])) as unknown as AmountsIn;
// allow a buffer to avoid slippage, 5%
const maxPayment = Number(amountIn * 1.05).toFixed();
/**
* 3. Create and dispatch `feeProxy.callWithFeePreferences` extrinsic
*/
logger.info(
{
parameters: {
paymentAsset: ASTO_ASSET_ID,
maxPayment,
call: remarkCall.toJSON(),
},
},
`create a "feeProxy.callWithFeePreferences"`
);
const feeProxyCall = api.tx.feeProxy.callWithFeePreferences(
ASTO_ASSET_ID,
maxPayment,
remarkCall
);
logger.info(`dispatch extrinsic from caller="${caller.address}"`);
const { result, extrinsicId } = await sendExtrinsic(feeProxyCall, caller, { log: logger });
const [proxyEvent, remarkEvent] = filterExtrinsicEvents(result.events, [
"FeeProxy.CallWithFeePreferences",
"System.Remarked",
]);
logger.info(
{
result: {
extrinsicId,
blockNumber: result.blockNumber,
proxyEvent: formatEventData(proxyEvent.event),
remarkEvent: formatEventData(remarkEvent.event),
},
},
"receive result"
);
});package.json
json
{
"scripts": {
"call": "tsx --tsconfig ../../../tsconfig.json",
"call:callSystemRemark": "pnpm call src/callSystemRemark.ts",
"call:callProxyExtrinsic": "pnpm call src/callProxyExtrinsic.ts",
"call:callBatchAll": "pnpm call src/callBatchAll.ts",
"call:callEVMCall": "pnpm call src/callEVMCall.ts",
"call:callProxyExtrinsicEVMCall": "pnpm call src/callProxyExtrinsicEVMCall.ts"
}
}Source:
futureversecom/trn-examples, examples/substrate/use-feeProxy (deleted) · commit 44c9e16, 2024-10-31 · Apache-2.0, license · part of Code examples