Wire up ICS-29 Fee to the React app
Our goal is to create a React component that will allow users to select their ICS-29 fee amount and pay it. The final component will look like this:
1. Create the State for ICS-29 Fee
We will do all our modifications in the src/components/IgntSend.tsx
file. First, we need to create a state for the fee amount. Add the following line to the IgntSend
component:
interface TxData {
receiver: string;
ch: string;
amounts: Array<Amount>;
memo: string;
fees: Array<Amount>;
+ relayerFee: Array<Amount>;
}
const initialState: State = {
tx: {
receiver: "",
ch: "",
amounts: [],
memo: "",
fees: [],
+ relayerFee: [],
},
currentUIState: UI_STATE.SEND,
advancedOpen: false,
};
2. Add the ICS-29 Fee UI
Next, we need to add a functional UI which updates the fee amount in the state. Add the following code to the IgntSend
component:
const handleTxFeesUpdate = (selected: Amount[]) => {
setState((oldState) => {
const tx = oldState.tx;
tx.fees = selected;
return { ...oldState, tx };
});
};
+ const handleTxRelayerFeesUpdate = (selected: Amount[]) => {
+ setState((oldState) => {
+ const tx = oldState.tx;
+ tx.relayerFee = selected;
+ return { ...oldState, tx };
+ });
+ };
<div className="text-xs text-gray-600">Channel</div>
<div className="input-wrapper">
<input
className="mt-1 py-2 px-4 h-12 bg-gray-100 border-xs text-base leading-tight w-full rounded-xl outline-0"
placeholder="Enter a channel"
onChange={(evt) => {
setState((oldState) => {
const tx = oldState.tx;
tx.ch = evt.target.value;
return { ...oldState, tx };
});
}}
/>
</div>
+
+ <div className="text-xs pb-2 mt-8">ICS-29 Relayer Fees</div>
+
+ <IgntAmountSelect
+ className="token-selector"
+ selected={state.tx.relayerFee}
+ balances={balances.assets as Amount[]}
+ update={handleTxRelayerFeesUpdate}
+ />
At this point, you should be able to see the ICS-29 fee UI in the app. See the diff up to this point here. However, the fee amount is not being used in the transaction. Let's fix that.
3. Add the ICS-29 Fee to the transaction
Since we will perform a MultiMsgTx
and follow the immediate incentivization flow, we must import the required msg constructors from the ts-client
.
export default function IgntSend(props: IgntSendProps) {
const [state, setState] = useState(initialState);
const client = useClient();
const sendMsgSend = client.CosmosBankV1Beta1.tx.sendMsgSend;
const sendMsgTransfer = client.IbcApplicationsTransferV1.tx.sendMsgTransfer;
+ const msgTransfer = client.IbcApplicationsTransferV1.tx.msgTransfer;
+ const msgPayPacketFee = client.IbcApplicationsFeeV1.tx.msgPayPacketFee;
const { address } = useAddressContext();
const { balances } = useAssets(100);
Recall that the PayPacketFee
message allows defining different tokens and amounts for each fee type (RecvFee
, AckFee
, and TimeoutFee
). We will use the same amount for all three fee types.
The amount used will be half the amount of the relayerFee
selected by the user. This is because one of AckFee
or TimeoutFee
will necessarily be refunded to the user since a packet either timeouts or receives acknowledgement but not both.
const sendTx = async (): Promise<void> => {
const fee: Array<Amount> = state.tx.fees.map((x) => ({
denom: x.denom,
amount: x.amount == "" ? "0" : x.amount,
}));
const amount: Array<Amount> = state.tx.amounts.map((x) => ({
denom: x.denom,
amount: x.amount == "" ? "0" : x.amount,
}));
+ const relayerFee: Array<Amount> = state.tx.relayerFee.map((x) => {
+ const intAmount = x.amount == "" ? 0 : parseInt(x.amount, 10);
+ const newAmount = Math.floor(intAmount / 2);
+ return {
+ denom: x.denom,
+ amount: newAmount.toString(),
+ };
+ });
+
Now that the fee amount is defined, we can build the tx. Currently, the way that the react app works is it checks whether or not a channel has been provided. If it has, it will send a MsgTransfer
message (isIBC = true
). Otherwise, it will send a MsgSend
message (isIBC = false
).
We will do something similar. We will check if relayerFee
has been provided, if it is provided, and if isIBC = true
, then we will build a MultiMsgTx
with PayPacketFee
and MsgTransfer
.
const memo = state.tx.memo;
const isIBC = state.tx.ch !== "";
+ const isFee = state.tx.relayerFee.length > 0;
+
let send;
let payload: any = {
amount,
toAddress: state.tx.receiver,
fromAddress: address,
};
setState((oldState) => ({ ...oldState, currentUIState: UI_STATE.TX_SIGNING }));
try {
if (isIBC) {
payload = {
...payload,
sourcePort: "transfer",
sourceChannel: state.tx.ch,
sender: address,
receiver: state.tx.receiver,
timeoutHeight: 0,
timeoutTimestamp: Long.fromNumber(new Date().getTime() + 60000).multiply(1000000),
token: state.tx.amounts[0],
};
- send = () =>
- sendMsgTransfer({
- value: payload,
- fee: { amount: fee as Readonly<Amount>[], gas: "200000" },
- memo,
- });
+ if (isFee) {
+ const payFeeMsg = msgPayPacketFee({
+ value: {
+ signer: address,
+ sourcePortId: "transfer",
+ sourceChannelId: state.tx.ch,
+ relayers: [],
+ fee: {
+ recvFee: relayerFee,
+ ackFee: relayerFee,
+ timeoutFee: relayerFee,
+ },
+ },
+ });
+
+ const transferMsg = msgTransfer({
+ value: payload,
+ });
+
+ send = () =>
+ client.signAndBroadcast(
+ [payFeeMsg, transferMsg],
+ { amount: fee as Readonly<Amount>[], gas: "200000" },
+ memo,
+ );
+ } else {
+ send = () =>
+ sendMsgTransfer({
+ value: payload,
+ fee: { amount: fee as Readonly<Amount>[], gas: "200000" },
+ memo,
+ });
+ }
} else {
See the diff up to this point here. We will test the UI in the next section.