diff --git a/examples/substrate/use-feeProxy/README.md b/examples/substrate/use-feeProxy/README.md index 23254b2..9771fbf 100644 --- a/examples/substrate/use-feeProxy/README.md +++ b/examples/substrate/use-feeProxy/README.md @@ -28,4 +28,7 @@ pnpm call:callBatchAll # `feeProxy.callWithFeePreferences` that wraps around `evm.call` pnpm call:callEVMCall +# `feeProxy.callWithFeePreferences` that wraps around `futurepass.proxyExtrinsic` then `emv.call` +pnpm call:callProxyExtrinsicEVMCall + ``` diff --git a/examples/substrate/use-feeProxy/package.json b/examples/substrate/use-feeProxy/package.json index 67b03eb..ed50513 100644 --- a/examples/substrate/use-feeProxy/package.json +++ b/examples/substrate/use-feeProxy/package.json @@ -4,6 +4,7 @@ "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:callEVMCall": "pnpm call src/callEVMCall.ts", + "call:callProxyExtrinsicEVMCall": "pnpm call src/callProxyExtrinsicEVMCall.ts" } } diff --git a/examples/substrate/use-feeProxy/src/callBatchAll.ts b/examples/substrate/use-feeProxy/src/callBatchAll.ts index 65b877f..1a66027 100644 --- a/examples/substrate/use-feeProxy/src/callBatchAll.ts +++ b/examples/substrate/use-feeProxy/src/callBatchAll.ts @@ -16,17 +16,33 @@ interface AmountsIn { * Assumes the caller has some 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, @@ -47,13 +63,27 @@ withChainApi("porcini", async (api, caller, logger) => { // 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 a feeProxy call with maxPayment="${maxPayment}"`); + logger.info(`dispatch extrinsic as caller="${caller.address}"`); const { result, extrinsicId } = await sendExtrinsic(feeProxyCall, caller, { log: logger }); const [proxyEvent, batchEvent, aliceTransferEvent, bobTransferEvent, charlieTransferEvent] = filterExtrinsicEvents(result.events, [ diff --git a/examples/substrate/use-feeProxy/src/callEVMCall.ts b/examples/substrate/use-feeProxy/src/callEVMCall.ts index ccfd7b0..6642305 100644 --- a/examples/substrate/use-feeProxy/src/callEVMCall.ts +++ b/examples/substrate/use-feeProxy/src/callEVMCall.ts @@ -1,9 +1,11 @@ +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"; @@ -15,53 +17,33 @@ interface AmountsIn { } /** - * Use `feeProxy.callWithFeePreferencs` to trigger `evm.call` call + * Use `feeProxy.callWithFeePreferencs` to trigger `evm.call` * - * Assumes the caller has some ASTO balance. + * Assumes the caller has some ASTO balance to pay for gas and some SYLO balance to demonstrate + * the transfer */ withChainApi("porcini", async (api, caller, logger) => { - // setup the evmCall - const { provider, wallet } = await provideEthersProvider("porcini"); - const transferToken = getERC20Contract(SYLO_ASSET_ID).connect(wallet); - const transferAmount = utils.parseUnits("1.0", 18); // 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 - ); - const evmCall = api.tx.evm.call( - caller.address, - transferToken.address, - transferInput, - 0, - transferEstimate.toString(), - maxFeePerGas.toString(), - 0, - null, - [] - ); + /** + * 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 feeProxyPaymentInfo = await feeProxyCallForEstimation.paymentInfo(caller.address); + 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(feeProxyPaymentInfo.partialFee.toString()) + const estimatedFee = BigNumber.from(paymentInfo.partialFee.toString()) .add(estimateGasCost) .toString(); - logger.info(`prepare a "evm.call" call with estimatedFee="${estimatedFee}"`); - // query the the `dex` to determine the `maxPayment` you are willing to pay const { Ok: [amountIn], @@ -72,12 +54,28 @@ withChainApi("porcini", async (api, caller, logger) => { // 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 a feeProxy call with maxPayment="${maxPayment}"`); + + logger.info(`dispatch extrinsic as caller="${caller.address}"`); const { result, extrinsicId } = await sendExtrinsic(feeProxyCall, caller, { log: logger }); - const [proxyEvent, aliceTransferEvent] = filterExtrinsicEvents(result.events, [ + const [proxyEvent, evmLogEvent, aliceTransferEvent] = filterExtrinsicEvents(result.events, [ "FeeProxy.CallWithFeePreferences", + "Evm.Log", { name: "Assets.Transferred", key: "to", data: { value: ALICE, type: "T::AccountId" } }, ]); @@ -87,9 +85,56 @@ withChainApi("porcini", async (api, caller, logger) => { 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, + }; +} diff --git a/examples/substrate/use-feeProxy/src/callProxyExtrinsic.ts b/examples/substrate/use-feeProxy/src/callProxyExtrinsic.ts index 10030b7..36b0d62 100644 --- a/examples/substrate/use-feeProxy/src/callProxyExtrinsic.ts +++ b/examples/substrate/use-feeProxy/src/callProxyExtrinsic.ts @@ -3,7 +3,7 @@ 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 "assert"; +import assert from "node:assert"; interface AmountsIn { Ok: [number, number]; @@ -13,9 +13,12 @@ interface AmountsIn { * 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 some ASTO balance in that account. + * Assumes the caller has a valid Futurepass account and some 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( { @@ -31,8 +34,20 @@ withChainApi("porcini", async (api, caller, logger) => { // can be any extrinsic, using `system.remarkWithEvent` for simplicity const remarkCall = api.tx.system.remarkWithEvent("Hello World"); - // wrap `remarkCall` with `proxyCall`, effetively request Futurepass account to pay for gas + 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, @@ -42,8 +57,6 @@ withChainApi("porcini", async (api, caller, logger) => { const paymentInfo = await feeProxyCallForEstimation.paymentInfo(caller.address); const estimatedFee = paymentInfo.partialFee.toString(); - console.log(estimatedFee); - // query the the `dex` to determine the `maxPayment` you are willing to pay const { Ok: [amountIn], @@ -54,13 +67,27 @@ withChainApi("porcini", async (api, caller, logger) => { // 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 a feeProxy call with maxPayment="${maxPayment}"`); + logger.info(`dispatch extrinsic as caller="${caller.address}"`); const { result, extrinsicId } = await sendExtrinsic(feeProxyCall, caller, { log: logger }); const [proxyEvent, futurepassEvent, remarkEvent] = filterExtrinsicEvents(result.events, [ "FeeProxy.CallWithFeePreferences", @@ -68,13 +95,16 @@ withChainApi("porcini", async (api, caller, logger) => { "System.Remarked", ]); - logger.info({ - result: { - extrinsicId, - blockNumber: result.blockNumber, - proxyEvent: formatEventData(proxyEvent.event), - futurepassEvent: formatEventData(futurepassEvent.event), - remarkEvent: formatEventData(remarkEvent.event), + logger.info( + { + result: { + extrinsicId, + blockNumber: result.blockNumber, + proxyEvent: formatEventData(proxyEvent.event), + futurepassEvent: formatEventData(futurepassEvent.event), + remarkEvent: formatEventData(remarkEvent.event), + }, }, - }); + "receive result" + ); }); diff --git a/examples/substrate/use-feeProxy/src/callProxyExtrinsicEVMCall.ts b/examples/substrate/use-feeProxy/src/callProxyExtrinsicEVMCall.ts new file mode 100644 index 0000000..29aee31 --- /dev/null +++ b/examples/substrate/use-feeProxy/src/callProxyExtrinsicEVMCall.ts @@ -0,0 +1,171 @@ +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, some ASTO balance to pay for gas and + * some 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)).unwrap(); + logger.info( + { + futurepass: { + holder: caller.address, + account: fpAccount.toString(), + }, + }, + "futurepass details" + ); + assert(fpAccount); + + 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 as 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, + }; +} diff --git a/examples/substrate/use-feeProxy/src/callSystemRemark.ts b/examples/substrate/use-feeProxy/src/callSystemRemark.ts index bd6803b..8e1b825 100644 --- a/examples/substrate/use-feeProxy/src/callSystemRemark.ts +++ b/examples/substrate/use-feeProxy/src/callSystemRemark.ts @@ -15,8 +15,23 @@ interface AmountsIn { * Assumes the caller has some 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, @@ -26,8 +41,6 @@ withChainApi("porcini", async (api, caller, logger) => { const paymentInfo = await feeProxyCallForEstimation.paymentInfo(caller.address); const estimatedFee = paymentInfo.partialFee.toString(); - logger.info(`prepare a "system.remark" call with estimatedFee="${estimatedFee}"`); - // query the the `dex` to determine the `maxPayment` you are willing to pay const { Ok: [amountIn], @@ -38,13 +51,27 @@ withChainApi("porcini", async (api, caller, logger) => { // 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 a feeProxy call with maxPayment="${maxPayment}"`); + logger.info(`dispatch extrinsic as caller="${caller.address}"`); const { result, extrinsicId } = await sendExtrinsic(feeProxyCall, caller, { log: logger }); const [proxyEvent, remarkEvent] = filterExtrinsicEvents(result.events, [ "FeeProxy.CallWithFeePreferences",