跳至主要内容
本页面使用机器翻译自英语,可能包含错误或不清楚的语言。如需最准确的信息,请参阅英文原文。由于更新频繁,部分内容可能与英文原文有出入。请加入我们在 Crowdin 上的努力,帮助我们改进本页面的翻译。 (Crowdin translation page, Contributing guide)

4. 在您的 dApp 或钱包中集成气体抽象

在本页中,您将了解如何将气体抽取 (GA) 功能集成到钱包中。 在本指南中,您将使用 Kaia SDK (ethers-ext) 在 Kaia 链上实现 GA 功能。

先决条件

  • Kaia 账户。

入门

本指南专为希望在 Kaia 上实施气体抽象 (GA) 的钱包开发人员设计。 您将经历一个真实世界的使用案例--执行一个应用级交易,用户可以在不持有任何本地 KAIA 的情况下申请空投 ERC20 代币来支付汽油费用。

您将学会如何

  • 为 GA 编制账目
  • 用 GA 支持的代币为账户提供资金
  • 利用气体抽取技术构建并执行审批和交换交易

该系统可在Kaia 主网Kairos 测试网上无缝运行。 要跟上进度,您需要在任一网络上获得一些 GA 支持的 ERC-20 代币:

  • 在 Kaia 主网上,我们将使用 USDT
  • 在 Kairos Testnet 上,我们将使用 TEST 标记

使用受支持的 GA 代币为您的账户充值

凯亚主网

获取 USDT:

  • 您可以从这些支持 Kaia-Compatible 代币的 集中式交易所 (CEXes) 购买或转移 USDT。
  • 确保您收到的 USDT 是 Kaia GA 支持的 ERC-20 版本。

开罗测试网

获取测试代币:

  1. 在 Kaiascan 上打开 ERC20 水龙头
  2. 转到 "合同"选项卡,然后选择 "撰写合同"。
  3. 找到 claim(token) 函数
  4. 输入 Kairos 上受支持的 GA 令牌的地址(本指南使用 TEST 的地址)。
  5. 点击查询提交请求

您将很快收到 TEST 令牌。

步骤 1:设置项目并安装 ethers-ext 和 ethers.js


mkdir kaia-ga-example
cd kaia-ga-example
npm init -y
npm install --save @kaiachain/ethers-ext ethers@6 dotenv

步骤 2:设置提供程序和钱包实例

卡娅

新建一个名为 kaia-ga.js 的文件,并粘贴以下代码:


const ethers = require("ethers"); // ethers v6
const { Wallet, gasless } = require("@kaiachain/ethers-ext/v6");
require('dotenv').config()
// Replace with your wallet address and private key in your .env file
const senderAddr = "PASTE SENDER ADDRESS";
const senderPriv = process.env.SENDER_PK;
const provider = new ethers.JsonRpcProvider(
"https://kaia.blockpi.network/v1/rpc/public"
);
const wallet = new Wallet(senderPriv, provider);

启示录

新建一个名为 kairos-ga.js 的文件,并粘贴下面的代码:


const ethers = require("ethers"); // ethers v6
const { Wallet, gasless } = require("@kaiachain/ethers-ext/v6");
require('dotenv').config()
// Replace with your wallet address and private key in your .env file
const senderAddr = "PASTE SENDER ADDRESS";
const senderPriv = process.env.SENDER_PK;
const provider = new ethers.JsonRpcProvider(
"https://responsive-green-emerald.kaia-kairos.quiknode.pro"
);
const wallet = new Wallet(senderPriv, provider);

备注

步骤 2 至 6 共同构成一个完整的可执行流程。 按顺序将每个区块复制到同一文件中。\

第 3 步:配置合同以估算索赔费用和令牌支持

在这一步中,我们要估算执行索偿空投交易的成本,以便用代币交换来弥补。 这一估算成本被称为 AppTxFee--发送方需要从交换中获得的 KAIA(单位:Wei)金额,以便为后续的应用级交易(在本例中为空投请求)提供资金。
我们还将准备和配置所有必要的合同实例:

  • 验证 GaslessSwapRouter 是否支持所选的 ERC20 令牌。
  • 读取路由器当前收取的佣金率
  • 获取路由器地址,用于交换和审批步骤。

这些参数对于在接下来的步骤中设置有效和可执行的无气调换至关重要。

卡娅


// Replace with ERC20 token address to be spent
const tokenAddr = "0xd077A400968890Eacc75cdc901F0356c943e4fDb"; // USDT Token Contract Address
const ERC20_ABI = [
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
"function allowance(address owner, address spender) view returns (uint256)",
"function balanceOf(address owner) view returns (uint256)",
];
const CLAIM_GOLD_CONTRACT_ADDRESS = "0x8ce5130B137FD4e84F43e3E7aD34918aF8F70F6b";
// MINIMAL ABI CLAIM GOLD CONTRACT
const CLAIM_AIRDROP_ABI = [
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "mint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]
const CLAIM_AMOUNT = ethers.parseUnits("10", 18); // 10 tokens
async function main() {
// prepare encoded transaction
const iface = new ethers.Interface(CLAIM_AIRDROP_ABI);
const encodedData = iface.encodeFunctionData("mint", [
wallet.address,
CLAIM_AMOUNT,
]);
// estimate Gas
const estimatedGas = await provider.estimateGas({
to: CLAIM_GOLD_CONTRACT_ADDRESS,
from: wallet.address,
data: encodedData,
});
// gasPrice
const claimGasPrice = await provider.getFeeData();
console.log("Estimated Gas for claim:", estimatedGas.toString());
console.log("Estimated GasPrice for claim:", claimGasPrice.gasPrice.toString());
const gasFees = Number(estimatedGas) * Number(claimGasPrice.gasPrice);
console.log(`Gas fee: ${gasFees}`);
const gasFeesInEther = ethers.formatEther(gasFees.toString());
const appTxFee = ethers.parseEther(gasFeesInEther.toString()).toString();
// Query the environment
console.log(`Using token at address: ${tokenAddr}`);
const token = new ethers.Contract(tokenAddr, ERC20_ABI, provider);
const goldToken = new ethers.Contract(CLAIM_GOLD_CONTRACT_ADDRESS, CLAIM_AIRDROP_ABI, provider);
const tokenSymbol = await token.symbol();
const tokenDecimals = await token.decimals();
const tokenBalance = await token.balanceOf(senderAddr);
console.log(`\nInitial balance of the sender ${senderAddr}`);
console.log(
`- ${ethers.formatEther(await provider.getBalance(senderAddr))} KAIA`
);
console.log(
`- ${ethers.formatUnits(tokenBalance, tokenDecimals)} ${tokenSymbol}`
);
const router = await gasless.getGaslessSwapRouter(provider);
const routerAddr = await router.getAddress();
const isTokenSupported = await router.isTokenSupported(tokenAddr);
const commissionRate = Number(await router.commissionRate());
console.log(`\nGaslessSwapRouter address: ${routerAddr}`);
console.log(`- The token is supported: ${isTokenSupported}`);
console.log(`- Commission rate: ${commissionRate} bps`);
…….
}

启示录


// Replace with ERC20 token address to be spent
const tokenAddr = "0xcB00BA2cAb67A3771f9ca1Fa48FDa8881B457750"; // Kairos:TEST token
const ERC20_ABI = [
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
"function allowance(address owner, address spender) view returns (uint256)",
"function balanceOf(address owner) view returns (uint256)",
];
const CLAIM_GOLD_CONTRACT_ADDRESS = "0x18DfDEd9bb342519549c1dBAd832c0FCfF5F6F70";
// MINIMAL ABI CLAIM GOLD CONTRACT
const CLAIM_AIRDROP_ABI = [
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "mint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]
const CLAIM_AMOUNT = ethers.parseUnits("10", 18); // 10 tokens
async function main() {
// prepare encoded transaction
const iface = new ethers.Interface(CLAIM_AIRDROP_ABI);
const encodedData = iface.encodeFunctionData("mint", [
wallet.address,
CLAIM_AMOUNT,
]);
// estimate Gas
const estimatedGas = await provider.estimateGas({
to: CLAIM_GOLD_CONTRACT_ADDRESS,
from: wallet.address,
data: encodedData,
});
// gasPrice
const claimGasPrice = await provider.getFeeData();
console.log("Estimated Gas for claim:", estimatedGas.toString());
console.log("Estimated GasPrice for claim:", claimGasPrice.gasPrice.toString());
const gasFees = Number(estimatedGas) * Number(claimGasPrice.gasPrice);
console.log(`Gas fee: ${gasFees}`);
const gasFeesInEther = ethers.formatEther(gasFees.toString());
const appTxFee = ethers.parseEther(gasFeesInEther.toString()).toString();
// Query the environment
console.log(`Using token at address: ${tokenAddr}`);
const token = new ethers.Contract(tokenAddr, ERC20_ABI, provider);
const goldToken = new ethers.Contract(CLAIM_GOLD_CONTRACT_ADDRESS, CLAIM_AIRDROP_ABI, provider);
const tokenSymbol = await token.symbol();
const tokenDecimals = await token.decimals();
const tokenBalance = await token.balanceOf(senderAddr);
console.log(`\nInitial balance of the sender ${senderAddr}`);
console.log(
`- ${ethers.formatEther(await provider.getBalance(senderAddr))} KAIA`
);
console.log(
`- ${ethers.formatUnits(tokenBalance, tokenDecimals)} ${tokenSymbol}`
);
const router = await gasless.getGaslessSwapRouter(provider);
const routerAddr = await router.getAddress();
const isTokenSupported = await router.isTokenSupported(tokenAddr);
const commissionRate = Number(await router.commissionRate());
console.log(`\nGaslessSwapRouter address: ${routerAddr}`);
console.log(`- The token is supported: ${isTokenSupported}`);
console.log(`- Commission rate: ${commissionRate} bps`);
…….
}

步骤 4:准备 ApproveTx 和 SwapTx 交易

在这一步中,我们准备了两个基本交易,通过令牌转换实现气体抽象:

ApproveTx
智能合约在使用用户的 ERC20 代币之前,必须首先通过批准获得权限。 在这里,我们要检查发送方是否已经批准 GaslessSwapRouter 使用其代币。

  • 如果备付金为零,我们将生成一个 ApproveTx。
  • 如果津贴已经存在且足够,我们就跳过这一步,以节省汽油。

SwapTx
处理批准后,我们准备 SwapTx。 这是将 ERC20 代币转换为 KAIA 的交易,以支付最终索赔交易的天然气费用。
我们计算了三个关键值:

  • amountRepay 是支付所有相关交易(包括掉期本身)所需的 KAIA 确切金额
  • minAmountOut 是指在计入应用程序交易费和路由器佣金后,预期从掉期中获得的最低 KAIA 金额。
  • amountIn 是接收 minAmountOut 所需的 ERC20 代币金额,其中已考虑滑点因素

如果发送方的令牌余额不足以支付金额In,执行将停止,并提示用户为其账户充值。 这两个交易,即 ApproveTx 和 SwapTx,将被添加到交易列表中,我们将在接下来的步骤中看到,这两个交易将一起提交。

卡娅


……..
const gasPrice = Number((await provider.getFeeData()).gasPrice);
// If the sender hasn't approved, include ApproveTx first.
const allowance = await token.allowance(senderAddr, routerAddr);
const approveRequired = allowance == 0n;
const txs = [];
if (approveRequired) {
console.log("\nAdding ApproveTx because allowance is 0");
const approveTx = await gasless.getApproveTx(
provider,
senderAddr,
tokenAddr,
routerAddr,
gasPrice
);
txs.push(approveTx);
} else {
console.log("\nNo ApproveTx needed");
}
// - amountRepay (KAIA) is the cost of LendTx, ApproveTx, and SwapTx. The block miner shall fund it first,
// then the sender has to repay from the swap output.
// - minAmountOut (KAIA) is the required amount of the swap output. It must be enough to cover the amountRepay
// and pay the commission, still leaving appTxFee.
// - amountIn (token) is the amount of the token to be swapped to produce minAmountOut plus slippage.
console.log("\nCalculating the amount of the token to be swapped...");
console.log(`- gasPrice: ${ethers.formatUnits(gasPrice, "gwei")} gkei`);
const amountRepay = gasless.getAmountRepay(approveRequired, gasPrice);
console.log(`- amountRepay: ${ethers.formatEther(amountRepay)} KAIA`);
const minAmountOut = gasless.getMinAmountOut(
amountRepay,
appTxFee,
commissionRate
);
console.log(`- minAmountOut: ${ethers.formatEther(minAmountOut)} KAIA`);
const slippageBps = 50; // 0.5%
const amountIn = await gasless.getAmountIn(
router,
tokenAddr,
minAmountOut,
slippageBps
);
console.log(
`- amountIn: ${ethers.formatUnits(amountIn, tokenDecimals)} ${tokenSymbol}`
);
if (tokenBalance < amountIn) {
console.log(
`\nInsufficient balance of the token: ${ethers.formatUnits(
tokenBalance,
tokenDecimals
)} ${tokenSymbol}`
);
console.log(
`- Please transfer more ${tokenSymbol} to the sender ${senderAddr}`
);
return;
}
const swapTx = await gasless.getSwapTx(
provider,
senderAddr,
tokenAddr,
routerAddr,
amountIn,
minAmountOut,
amountRepay,
gasPrice,
approveRequired
);
txs.push(swapTx);
………

启示录


….
const gasPrice = Number((await provider.getFeeData()).gasPrice);
// If the sender hasn't approved, include ApproveTx first.
const allowance = await token.allowance(senderAddr, routerAddr);
const approveRequired = allowance == 0n;
const txs = [];
if (approveRequired) {
console.log("\nAdding ApproveTx because allowance is 0");
const approveTx = await gasless.getApproveTx(
provider,
senderAddr,
tokenAddr,
routerAddr,
gasPrice
);
txs.push(approveTx);
} else {
console.log("\nNo ApproveTx needed");
}
// - amountRepay (KAIA) is the cost of LendTx, ApproveTx, and SwapTx. The block miner shall fund it first,
// then the sender has to repay from the swap output.
// - minAmountOut (KAIA) is the required amount of the swap output. It must be enough to cover the amountRepay
// and pay the commission, still leaving appTxFee.
// - amountIn (token) is the amount of the token to be swapped to produce minAmountOut plus slippage.
console.log("\nCalculating the amount of the token to be swapped...");
console.log(`- gasPrice: ${ethers.formatUnits(gasPrice, "gwei")} gkei`);
const amountRepay = gasless.getAmountRepay(approveRequired, gasPrice);
console.log(`- amountRepay: ${ethers.formatEther(amountRepay)} KAIA`);
const minAmountOut = gasless.getMinAmountOut(
amountRepay,
appTxFee,
commissionRate
);
console.log(`- minAmountOut: ${ethers.formatEther(minAmountOut)} KAIA`);
const slippageBps = 50; // 0.5%
const amountIn = await gasless.getAmountIn(
router,
tokenAddr,
minAmountOut,
slippageBps
);
console.log(
`- amountIn: ${ethers.formatUnits(amountIn, tokenDecimals)} ${tokenSymbol}`
);
if (tokenBalance < amountIn) {
console.log(
`\nInsufficient balance of the token: ${ethers.formatUnits(
tokenBalance,
tokenDecimals
)} ${tokenSymbol}`
);
console.log(
`- Please transfer more ${tokenSymbol} to the sender ${senderAddr}`
);
return;
}
const swapTx = await gasless.getSwapTx(
provider,
senderAddr,
tokenAddr,
routerAddr,
amountIn,
minAmountOut,
amountRepay,
gasPrice,
approveRequired
);
txs.push(swapTx);
….

步骤 5:执行 ApproveTx 和 SwapTx 交易

现在,ApproveTxSwapTx 都已准备就绪并添加到事务列表中,我们可以使用 Kaia 的气体抽象功能将它们一起执行。

wallet.sendTransactions(txs) 函数通过批量提交交易来处理这一过程。 在引擎盖下,它利用kaia_sendRawTransactions RPC 方法,该方法接受符合以太坊交易类型的签名 RLP 编码交易数组。

因此,它非常适合在一次原子操作中同时提交 ApproveTx 和 SwapTx。

卡娅


……..
console.log("\nSending gasless transactions...");
const sentTxs = await wallet.sendTransactions(txs);
for (const tx of sentTxs) {
console.log(`- Tx sent: (nonce: ${tx.nonce}) ${tx.hash}`);
}
console.log("\nWaiting for transactions to be mined...");
let blockNum = 0;
for (const sentTx of sentTxs) {
const receipt = await sentTx.wait();
console.log(`- Tx mined at block ${receipt.blockNumber}`);
blockNum = receipt.blockNumber;
}
………

启示录


…….
console.log("\nSending gasless transactions...");
const sentTxs = await wallet.sendTransactions(txs);
for (const tx of sentTxs) {
console.log(`- Tx sent: (nonce: ${tx.nonce}) ${tx.hash}`);
}
console.log("\nWaiting for transactions to be mined...");
let blockNum = 0;
for (const sentTx of sentTxs) {
const receipt = await sentTx.wait();
console.log(`- Tx mined at block ${receipt.blockNumber}`);
blockNum = receipt.blockNumber;
}
……

步骤 6:执行申请交易(索赔空投)

交换完成并收到 AppTxFee 后,现在就可以触发主应用程序事务--在本例中就是索赔空投功能。

首先,使用目标合同地址、编码呼叫数据、估计天然气和当前天然气价格构建交易对象:

卡娅


const claimTxObject = {
to: CLAIM_GOLD_CONTRACT_ADDRESS,
data: encodedData,
gasLimit: estimatedGas,
gasPrice: claimGasPrice.gasPrice,
};

启示录


…….
const claimTxObject = {
to: CLAIM_GOLD_CONTRACT_ADDRESS,
data: encodedData,
gasLimit: estimatedGas,
gasPrice: claimGasPrice.gasPrice,
};

使用钱包发送交易:

卡娅


…….
console.log("\nClaiming airdrop...");
const claimAirdropTx = await wallet.sendTransaction(claimTxObject);
const txx = await claimAirdropTx.wait()
console.log("ClaimAirdrop Tx Hash: ", txx.hash);
……

启示录


console.log("\nClaiming airdrop...");
const claimAirdropTx = await wallet.sendTransaction(claimTxObject);
const txx = await claimAirdropTx.wait()
console.log("ClaimAirdrop Tx Hash: ", txx.hash);

交易完成后,您可能需要检查发送者的最新余额,以确认空投已被成功认领:

卡娅


console.log(`\nFinal balance of the sender ${senderAddr}`);
console.log(`${ethers.formatEther(await provider.getBalance(senderAddr))} KAIA`);
console.log(`${ethers.formatUnits(await token.balanceOf(senderAddr), tokenDecimals)} ${tokenSymbol}`);
console.log(`${ethers.formatUnits(await goldToken.balanceOf(senderAddr), 18)} GOLD tokens`);

启示录


console.log(`\nFinal balance of the sender ${senderAddr}`);
console.log(`${ethers.formatEther(await provider.getBalance(senderAddr))} KAIA`);
console.log(`${ethers.formatUnits(await token.balanceOf(senderAddr), tokenDecimals)} ${tokenSymbol}`);
console.log(`${ethers.formatUnits(await goldToken.balanceOf(senderAddr), tokenDecimals)} GOLD tokens`);

这种平衡检查可确保用户从索赔空投呼叫中收到预期的 GOLD 令牌,并确认 AppTxFee 交换机制按预期运行。

完整代码:

想直接运行完整示例? 将所有步骤合并复制到下面的完整脚本中。

卡娅


const ethers = require("ethers"); // ethers v6
const { Wallet, gasless } = require("@kaiachain/ethers-ext/v6");
require('dotenv').config()
// Replace with your wallet address and private key
const senderAddr = "PASTE SENDER ADDRESS";
const senderPriv = process.env.SENDER_PK;
const provider = new ethers.JsonRpcProvider(
"https://kaia.blockpi.network/v1/rpc/public"
);
const wallet = new Wallet(senderPriv, provider);
// Replace with ERC20 token address to be spent
const tokenAddr = "0xd077A400968890Eacc75cdc901F0356c943e4fDb"; // USDT Token Contract Address
const ERC20_ABI = [
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
"function allowance(address owner, address spender) view returns (uint256)",
"function balanceOf(address owner) view returns (uint256)",
];
const CLAIM_GOLD_CONTRACT_ADDRESS = "0x8ce5130B137FD4e84F43e3E7aD34918aF8F70F6b";
// MINIMAL CLAIM GOLD CONTRACT ABI
const CLAIM_AIRDROP_ABI = [
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "mint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]
const CLAIM_AMOUNT = ethers.parseUnits("10", 18); // 10 tokens
async function main() {
// prepare encoded transaction
const iface = new ethers.Interface(CLAIM_AIRDROP_ABI);
const encodedData = iface.encodeFunctionData("mint", [
wallet.address,
CLAIM_AMOUNT,
]);
// estimate Gas
const estimatedGas = await provider.estimateGas({
to: CLAIM_GOLD_CONTRACT_ADDRESS,
from: wallet.address,
data: encodedData,
});
// gasPrice
const claimGasPrice = await provider.getFeeData();
console.log("Estimated Gas for claim:", estimatedGas.toString());
console.log("Estimated GasPrice for claim:", claimGasPrice.gasPrice.toString());
const gasFees = Number(estimatedGas) * Number(claimGasPrice.gasPrice);
console.log(`Gas fee: ${gasFees}`);
const gasFeesInEther = ethers.formatEther(gasFees.toString());
const appTxFee = ethers.parseEther(gasFeesInEther.toString()).toString();
// Query the environment
console.log(`Using token at address: ${tokenAddr}`);
const token = new ethers.Contract(tokenAddr, ERC20_ABI, provider);
const goldToken = new ethers.Contract(CLAIM_GOLD_CONTRACT_ADDRESS, CLAIM_AIRDROP_ABI, provider);
const tokenSymbol = await token.symbol();
const tokenDecimals = await token.decimals();
const tokenBalance = await token.balanceOf(senderAddr);
console.log(`\nInitial balance of the sender ${senderAddr}`);
console.log(
`- ${ethers.formatEther(await provider.getBalance(senderAddr))} KAIA`
);
console.log(
`- ${ethers.formatUnits(tokenBalance, tokenDecimals)} ${tokenSymbol}`
);
const router = await gasless.getGaslessSwapRouter(provider);
const routerAddr = await router.getAddress();
const isTokenSupported = await router.isTokenSupported(tokenAddr);
const commissionRate = Number(await router.commissionRate());
console.log(`\nGaslessSwapRouter address: ${routerAddr}`);
console.log(`- The token is supported: ${isTokenSupported}`);
console.log(`- Commission rate: ${commissionRate} bps`);
const gasPrice = Number((await provider.getFeeData()).gasPrice);
// If sender hasn't approved, include ApproveTx first.
const allowance = await token.allowance(senderAddr, routerAddr);
const approveRequired = allowance == 0n;
const txs = [];
if (approveRequired) {
console.log("\nAdding ApproveTx because allowance is 0");
const approveTx = await gasless.getApproveTx(
provider,
senderAddr,
tokenAddr,
routerAddr,
gasPrice
);
txs.push(approveTx);
} else {
console.log("\nNo ApproveTx needed");
}
// - amountRepay (KAIA) is the cost of LendTx, ApproveTx, and SwapTx. The block miner shall fund it first,
// then the sender has to repay from the swap output.
// - minAmountOut (KAIA) is the required amount of the swap output. It must be enough to cover the amountRepay
// and pay the commission, still leaving appTxFee.
// - amountIn (token) is the amount of the token to be swapped to produce minAmountOut plus slippage.
console.log("\nCalculating the amount of the token to be swapped...");
console.log(`- gasPrice: ${ethers.formatUnits(gasPrice, "gwei")} gkei`);
const amountRepay = gasless.getAmountRepay(approveRequired, gasPrice);
console.log(`- amountRepay: ${ethers.formatEther(amountRepay)} KAIA`);
const minAmountOut = gasless.getMinAmountOut(
amountRepay,
appTxFee,
commissionRate
);
console.log(`- minAmountOut: ${ethers.formatEther(minAmountOut)} KAIA`);
const slippageBps = 50; // 0.5%
const amountIn = await gasless.getAmountIn(
router,
tokenAddr,
minAmountOut,
slippageBps
);
console.log(
`- amountIn: ${ethers.formatUnits(amountIn, tokenDecimals)} ${tokenSymbol}`
);
if (tokenBalance < amountIn) {
console.log(
`\nInsufficient balance of the token: ${ethers.formatUnits(
tokenBalance,
tokenDecimals
)} ${tokenSymbol}`
);
console.log(
`- Please transfer more ${tokenSymbol} to the sender ${senderAddr}`
);
return;
}
const swapTx = await gasless.getSwapTx(
provider,
senderAddr,
tokenAddr,
routerAddr,
amountIn,
minAmountOut,
amountRepay,
gasPrice,
approveRequired
);
txs.push(swapTx);
console.log("\nSending gasless transactions...");
const sentTxs = await wallet.sendTransactions(txs);
for (const tx of sentTxs) {
console.log(`- Tx sent: (nonce: ${tx.nonce}) ${tx.hash}`);
}
console.log("\nWaiting for transactions to be mined...");
let blockNum = 0;
for (const sentTx of sentTxs) {
const receipt = await sentTx.wait();
console.log(`- Tx mined at block ${receipt.blockNumber}`);
blockNum = receipt.blockNumber;
}
console.log("\nListing the block's transactions related to the sender...");
const block = await provider.getBlock(blockNum, true);
const names = {
[senderAddr.toLowerCase()]: "sender",
[tokenAddr.toLowerCase()]: "token",
[routerAddr.toLowerCase()]: "router",
};
for (const txhash of block.transactions) {
const tx = await provider.getTransaction(txhash);
const fromName = names[tx.from.toLowerCase()] || tx.from;
const toName = names[tx.to.toLowerCase()] || tx.to;
if (fromName != tx.from || toName != tx.to) {
console.log(`- Tx ${tx.hash}: ${fromName} => ${toName}`);
}
}
// Construct transaction object
const claimTxObject = {
to: CLAIM_GOLD_CONTRACT_ADDRESS,
data: encodedData,
gasLimit: estimatedGas,
gasPrice: claimGasPrice.gasPrice,
};
console.log("\nClaiming airdrop...");
const claimAirdropTx = await wallet.sendTransaction(claimTxObject);
const txx = await claimAirdropTx.wait()
console.log("ClaimAirdrop Tx Hash: ", txx.hash);
console.log(`\nFinal balance of the sender ${senderAddr}`);
console.log(
`- ${ethers.formatEther(await provider.getBalance(senderAddr))} KAIA`
);
console.log(
`- ${ethers.formatUnits(
await token.balanceOf(senderAddr),
tokenDecimals
)} ${tokenSymbol}`
);
console.log(
`- ${ethers.formatUnits(
await goldToken.balanceOf(senderAddr),
18
)} GOLD tokens`
);
}
main().catch(console.error);

启示录


const ethers = require("ethers"); // ethers v6
const { Wallet, gasless } = require("@kaiachain/ethers-ext/v6");
require('dotenv').config()
// Replace with your wallet address and private key
const senderAddr = "PASTE SENDER ADDRESS";
const senderPriv = process.env.SENDER_PK;
const provider = new ethers.JsonRpcProvider(
"https://responsive-green-emerald.kaia-kairos.quiknode.pro"
);
const wallet = new Wallet(senderPriv, provider);
// Replace with ERC20 token address to be spent
const tokenAddr = "0xcB00BA2cAb67A3771f9ca1Fa48FDa8881B457750"; // Kairos:TEST token
const ERC20_ABI = [
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
"function allowance(address owner, address spender) view returns (uint256)",
"function balanceOf(address owner) view returns (uint256)",
];
const CLAIM_GOLD_CONTRACT_ADDRESS = "0x18DfDEd9bb342519549c1dBAd832c0FCfF5F6F70";
// MINIMAL CLAIM GOLD CONTRACT ABI
const CLAIM_AIRDROP_ABI = [
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "mint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]
const CLAIM_AMOUNT = ethers.parseUnits("10", 18); // 10 tokens
async function main() {
// prepare encoded transaction
const iface = new ethers.Interface(CLAIM_AIRDROP_ABI);
const encodedData = iface.encodeFunctionData("mint", [
wallet.address,
CLAIM_AMOUNT,
]);
// estimate Gas
const estimatedGas = await provider.estimateGas({
to: CLAIM_GOLD_CONTRACT_ADDRESS,
from: wallet.address,
data: encodedData,
});
// gasPrice
const claimGasPrice = await provider.getFeeData();
console.log("Estimated Gas for claim:", estimatedGas.toString());
console.log("Estimated GasPrice for claim:", claimGasPrice.gasPrice.toString());
const gasFees = Number(estimatedGas) * Number(claimGasPrice.gasPrice);
console.log(`Gas fee: ${gasFees}`);
const gasFeesInEther = ethers.formatEther(gasFees.toString());
const appTxFee = ethers.parseEther(gasFeesInEther.toString()).toString();
// Query the environment
console.log(`Using token at address: ${tokenAddr}`);
const token = new ethers.Contract(tokenAddr, ERC20_ABI, provider);
const goldToken = new ethers.Contract(CLAIM_GOLD_CONTRACT_ADDRESS, CLAIM_AIRDROP_ABI, provider);
const tokenSymbol = await token.symbol();
const tokenDecimals = await token.decimals();
const tokenBalance = await token.balanceOf(senderAddr);
console.log(`\nInitial balance of the sender ${senderAddr}`);
console.log(
`- ${ethers.formatEther(await provider.getBalance(senderAddr))} KAIA`
);
console.log(
`- ${ethers.formatUnits(tokenBalance, tokenDecimals)} ${tokenSymbol}`
);
const router = await gasless.getGaslessSwapRouter(provider);
const routerAddr = await router.getAddress();
const isTokenSupported = await router.isTokenSupported(tokenAddr);
const commissionRate = Number(await router.commissionRate());
console.log(`\nGaslessSwapRouter address: ${routerAddr}`);
console.log(`- The token is supported: ${isTokenSupported}`);
console.log(`- Commission rate: ${commissionRate} bps`);
const gasPrice = Number((await provider.getFeeData()).gasPrice);
// If sender hasn't approved, include ApproveTx first.
const allowance = await token.allowance(senderAddr, routerAddr);
const approveRequired = allowance == 0n;
const txs = [];
if (approveRequired) {
console.log("\nAdding ApproveTx because allowance is 0");
const approveTx = await gasless.getApproveTx(
provider,
senderAddr,
tokenAddr,
routerAddr,
gasPrice
);
txs.push(approveTx);
} else {
console.log("\nNo ApproveTx needed");
}
// - amountRepay (KAIA) is the cost of LendTx, ApproveTx, and SwapTx. The block miner shall fund it first,
// then the sender has to repay from the swap output.
// - minAmountOut (KAIA) is the required amount of the swap output. It must be enough to cover the amountRepay
// and pay the commission, still leaving appTxFee.
// - amountIn (token) is the amount of the token to be swapped to produce minAmountOut plus slippage.
console.log("\nCalculating the amount of the token to be swapped...");
console.log(`- gasPrice: ${ethers.formatUnits(gasPrice, "gwei")} gkei`);
const amountRepay = gasless.getAmountRepay(approveRequired, gasPrice);
console.log(`- amountRepay: ${ethers.formatEther(amountRepay)} KAIA`);
const minAmountOut = gasless.getMinAmountOut(
amountRepay,
appTxFee,
commissionRate
);
console.log(`- minAmountOut: ${ethers.formatEther(minAmountOut)} KAIA`);
const slippageBps = 50; // 0.5%
const amountIn = await gasless.getAmountIn(
router,
tokenAddr,
minAmountOut,
slippageBps
);
console.log(
`- amountIn: ${ethers.formatUnits(amountIn, tokenDecimals)} ${tokenSymbol}`
);
if (tokenBalance < amountIn) {
console.log(
`\nInsufficient balance of the token: ${ethers.formatUnits(
tokenBalance,
tokenDecimals
)} ${tokenSymbol}`
);
console.log(
`- Please transfer more ${tokenSymbol} to the sender ${senderAddr}`
);
return;
}
const swapTx = await gasless.getSwapTx(
provider,
senderAddr,
tokenAddr,
routerAddr,
amountIn,
minAmountOut,
amountRepay,
gasPrice,
approveRequired
);
txs.push(swapTx);
console.log("\nSending gasless transactions...");
const sentTxs = await wallet.sendTransactions(txs);
for (const tx of sentTxs) {
console.log(`- Tx sent: (nonce: ${tx.nonce}) ${tx.hash}`);
}
console.log("\nWaiting for transactions to be mined...");
let blockNum = 0;
for (const sentTx of sentTxs) {
const receipt = await sentTx.wait();
console.log(`- Tx mined at block ${receipt.blockNumber}`);
blockNum = receipt.blockNumber;
}
console.log("\nListing the block's transactions related to the sender...");
const block = await provider.getBlock(blockNum, true);
const names = {
[senderAddr.toLowerCase()]: "sender",
[tokenAddr.toLowerCase()]: "token",
[routerAddr.toLowerCase()]: "router",
};
for (const txhash of block.transactions) {
const tx = await provider.getTransaction(txhash);
const fromName = names[tx.from.toLowerCase()] || tx.from;
const toName = names[tx.to.toLowerCase()] || tx.to;
if (fromName != tx.from || toName != tx.to) {
console.log(`- Tx ${tx.hash}: ${fromName} => ${toName}`);
}
}
// Construct transaction object
const claimTxObject = {
to: CLAIM_GOLD_CONTRACT_ADDRESS,
data: encodedData,
gasLimit: estimatedGas,
gasPrice: claimGasPrice.gasPrice,
};
console.log("\nClaiming airdrop...");
const claimAirdropTx = await wallet.sendTransaction(claimTxObject);
const txx = await claimAirdropTx.wait()
console.log("ClaimAirdrop Tx Hash: ", txx.hash);
console.log(`\nFinal balance of the sender ${senderAddr}`);
console.log(
`- ${ethers.formatEther(await provider.getBalance(senderAddr))} KAIA`
);
console.log(
`- ${ethers.formatUnits(
await token.balanceOf(senderAddr),
tokenDecimals
)} ${tokenSymbol}`
);
console.log(
`- ${ethers.formatUnits(
await goldToken.balanceOf(senderAddr),
tokenDecimals
)} GOLD tokens`
);
}
main().catch(console.error);

第 7 步:查看代码运行

现在,您已经完成了设置并准备好了事务,是时候运行代码并观察结果了。

凯娅

在终端运行 `node kaia-ga.js

输出:

开罗

在终端运行 `node kairos-ga.js

输出:

结论

您刚刚完成了在 Kaia 上执行天然气抽取交易的完整演练。 从建立账户、使用支持的代币为账户注资,到建立和提交交易,一切都无需用户持有本地 KAIA。 你们还看到了一个真实世界的流程,用户可以在完全没有气体的情况下接收和认领空投。

通过使用 Kaia 的气体抽象功能,您现在可以使用零本地令牌登录用户--消除摩擦,为 Web3 应用程序创造更流畅的体验。

让这个页面变得更好