什么是空投?
每一个新的 DApp 上线,第一件要做的事就是收集用户。
而收集用户的方式有哪些呢?原则上很简单:提供利益、提供价值。
像上一篇文章中介绍的“水龙头”就是一种收集用户的方式。
但水龙头是用户主动向项目方领取的,门槛比较低。而且黏性不高,用户领完币仍然可能不会使用你的 DApp。就像打卡一样。
空投是一种门槛相对更高一点的玩法。它是项目方主动给用户提供奖励。一般来说,空投的奖励会比水龙头高多了。
只有早期参与 DApp 的活跃用户,才会得到项目方的空投。空投奖励通常是以代币的形式。
那些整天在群里问「群主,有没有空投推荐?」的人,就是撸空投的。他们会参与多个 DApp,然后不停地交互。以此来得到项目方的空投。
但仅靠使用 DApp 是不够的,项目方为了保证真实活跃,会对空投玩法设置一些条件。比如必须要加入 discord、必须绑定推特和邮箱注册。
所以这就形成了一套账户,也就是常说的空投四件套:地址、discord、推特、邮箱。
空投经济,不仅养活了项目方、养活了撸空投的人。还顺便养活了一帮专门卖空投项目的人、专门养号卖号的人,这就是一个商业模式的闭环。
但依我个人经验来看,空投没有那么好撸。运气成分占很大一部分。有人忙活了一年,几乎所有空闲时间都花在撸空投上了,结果一年到头也就赚个三五万。而有人可能只参与了几个空投项目,就赚了几十万。
那怎么判断一个空投项目是否值得撸呢?注意以下几点:
- 首先项目方必须明确表示会有空投,或者强烈暗示。
- 项目必须有潜在价值,垃圾项目没必要撸。大部分项目都只能等代币上了交易所才能赚到钱,否则就是空气币。
- 参与项目的门槛相对要高,如果门槛低撸空投的人太多,分不到多少代币。
我并不建议大家撸空投,因为水太深了。
尽管如今还有大量的人不懂 web3 的种种概念。但墙内和墙外是两个世界,我看到的 web3 空投领域,已经发展地非常成熟了。老玩家已经玩起了各种黑科技,比如使用麒麟同步器批量操作、使用Hubstudio 处理浏览器指纹、使用 MaxProxy 做 IP 代理、使用接码平台接码等等。在效率方面几乎完全碾压普通玩家。但这些对空投大师来说也只是基操,还有更离谱的黑科技,我就不多讲了。总之,普通玩家和资深玩家的差距,已经不是在一个位面上了。
重申一遍,我不建议普通人撸空投,我也不会推荐任何空投项目。
Web3 中的绝大多数项目,基本上只有两大核心。一是营销、一是技术。
说白了,空投和水龙头一样,都是一种营销手段。
我们了解完空投的概念和玩法,下面就开始讲技术。
批量转账合约设计
空投的需求,其实就是一个批量转账。所以我们要设计一个批量转账的合约。
批量转账有两种方式,第一种是 N 对 1,第二种是 N 对 N。
转账这件事,就只有三个要素。谁转的?转给谁?转多少?
谁转的?很明显就是项目方。
转给谁?一堆合约地址。应该是一个地址类型的数组。
转多少?可以是同一个金额,也可以每个地址的金额不一样。
现在我们了解了需求,并设计好了玩法,接下来我们开始具体实现。
智能合约实现
扣除模式
在调用者方面,我们可以设计两种模式。
- 第一种是仅合约发布者可以发起空投,空投的代币需要合约发起者先转入空投合约,由空投合约进行发起。
- 第二种是任何人都可以发起空投,空投的代币由合约调用者直接支出。如果是任何人都可以空投的话,可以收取一定的手续费。
发放模式
在代币发放方面也有两种方式。
- 第一种是多个地址对应一个转账金额。
- 第二张是一个地址对应一个转账金额。
基于以上两种视角的不同模式,所以合约可以有两种实现。
仅合约拥有者可调用版本
合约实现如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC20.sol";
contract Airdrop {
IERC20 public tokenContract; // 代币合约
address public owner; // 合约发布者
constructor(address _tokenContractAddress) {
tokenContract = IERC20(_tokenContractAddress);
owner = msg.sender;
}
// 空投代币,多个地址对应一个数量
function oneToMany(address[] memory _to, uint256 _amount) public {
// 只有合约发布者可以调用
require(msg.sender == owner, "Only the owner can airdrop tokens");
// 验证合约中的代币数量是否足够
uint256 totalAmount = _amount * _to.length;
require(
tokenContract.balanceOf(address(this)) >= totalAmount,
"Not enough tokens in the contract"
);
// 空投代币
for (uint256 i = 0; i < _to.length; i++) {
tokenContract.transfer(_to[i], _amount);
}
}
// 空投代币,一个地址对应一个数量
function oneToOne(address[] memory _to, uint256[] memory _amount) public {
// 只有合约发布者可以调用
require(msg.sender == owner, "Only the owner can airdrop tokens");
// 验证数组长度是否相等
require(
_to.length == _amount.length,
"The length of the two arrays must be the same"
);
// 验证合约中的代币是否足够
uint256 totalAmount = 0;
for (uint256 i = 0; i < _amount.length; i++) {
totalAmount += _amount[i];
}
require(
tokenContract.balanceOf(address(this)) >= totalAmount,
"Not enough tokens in the contract"
);
// 空投代币
for (uint256 i = 0; i < _to.length; i++) {
tokenContract.transfer(_to[i], _amount[i]);
}
}
}
核心逻辑仅仅是一个 for 循环调用。同时需要检查余额是否足够;地址数量与金额数量是否匹配等。
代码中有详细的注释,不需要多做解释。
任何人可调用版本(含手续费)
合约实现如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC20.sol";
// 任何人都可以调用,但是需要支付手续费
contract AirdropFree {
IERC20 public tokenContract; // 代币合约
address public owner; // 合约发布者
address private _marketingWalletAddress; // 营销钱包地址,用于收取手续费
uint256 private _feeRate = 10; // 手续费比例,单位:万分之一
constructor(
address _tokenContractAddress,
address _marketingWallet,
uint256 _fee
) {
tokenContract = IERC20(_tokenContractAddress);
_marketingWalletAddress = _marketingWallet;
_feeRate = _fee;
owner = msg.sender;
}
// 空投代币,多个地址对应一个数量
function oneToMany(address[] memory _to, uint256 _amount) public {
uint256 totalAmount = _amount * _to.length;
// 计算手续费
uint256 fee = (totalAmount * _feeRate) / 10000;
// 增加手续费
totalAmount += fee;
// 验证调用者的代币数量是否足够
require(
tokenContract.balanceOf(msg.sender) >= totalAmount,
"Not enough tokens in the address"
);
// 检查调用者授权数量是否足够
require(
tokenContract.allowance(msg.sender, address(this)) >= totalAmount,
"Not enough tokens approved"
);
// 空投代币
for (uint256 i = 0; i < _to.length; i++) {
tokenContract.transferFrom(msg.sender, _to[i], _amount);
}
// 转移手续费
tokenContract.transferFrom(msg.sender, _marketingWalletAddress, fee);
}
// 空投代币,一个地址对应一个数量
function oneToOne(address[] memory _to, uint256[] memory _amount) public {
// 验证数组长度是否相等
require(
_to.length == _amount.length,
"The length of the two arrays must be the same"
);
// 计算总数量
uint256 totalAmount = 0;
// 计算手续费
uint256 fee = 0;
for (uint256 i = 0; i < _amount.length; i++) {
totalAmount += _amount[i];
fee += (_amount[i] * _feeRate) / 10000;
}
// 增加手续费
totalAmount += fee;
// 验证调用者的代币数量是否足够
require(
tokenContract.balanceOf(msg.sender) >= totalAmount,
"Not enough tokens in the address"
);
// 检查调用者授权数量是否足够
require(
tokenContract.allowance(msg.sender, address(this)) >= totalAmount,
"Not enough tokens approved"
);
// 空投代币
for (uint256 i = 0; i < _to.length; i++) {
tokenContract.transferFrom(msg.sender, _to[i], _amount[i]);
}
// 转移手续费
tokenContract.transferFrom(msg.sender, _marketingWalletAddress, fee);
}
}
核心逻辑仅仅是一个 for 循环调用。同时需要检查余额是否足够;地址数量与金额数量是否匹配等。但这里附带了手续费的计算与抽取逻辑。
代码中有详细的注释,不需要多做解释。
合约测试
先来测试仅合约拥有者可调用版本:
const NoahToken = artifacts.require("NoahToken");
const Airdrop = artifacts.require("Airdrop");
contract("Airdrop", (accounts) => {
const [alice, bob, carol, dave] = accounts;
it("oneToMany", async () => {
// 发 Noah 币,发行 1024 个
const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '1024', { from: alice });
// 发空投合约
const airdropInstance = await Airdrop.new(noahTokenInstance.address, { from: alice });
// 给空投合约转账 100 个 Noah 币
const airdropTotalAmount = 100;
await noahTokenInstance.transfer(airdropInstance.address, airdropTotalAmount, { from: alice });
// 给 3 个账户发空投,每个账户 10 个 Noah 币
const amount = 10;
await airdropInstance.oneToMany([bob, carol, dave], amount, { from: alice });
// 检查 3 个账户的 Noah 币数量
const bobBalance = await noahTokenInstance.balanceOf(bob);
const carolBalance = await noahTokenInstance.balanceOf(carol);
const daveBalance = await noahTokenInstance.balanceOf(dave);
assert.equal(bobBalance.toString(), amount);
assert.equal(carolBalance.toString(), amount);
assert.equal(daveBalance.toString(), amount);
// 检查空投合约的 Noah 币数量
const airdropBalance = await noahTokenInstance.balanceOf(airdropInstance.address);
assert.equal(airdropBalance.toString(), airdropTotalAmount - 3 * amount);
});
it("oneToOne", async () => {
// 发 Noah 币,发行 1024 个
const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '1024', { from: alice });
// 发空投合约
const airdropInstance = await Airdrop.new(noahTokenInstance.address, { from: alice });
// 给空投合约转账 100 个 Noah 币
const airdropTotalAmount = 100;
await noahTokenInstance.transfer(airdropInstance.address, airdropTotalAmount, { from: alice });
// 给 3 个账户发空投,bob 10 个,carol 15 个,dave 20 个
const amounts = [10, 15, 20];
await airdropInstance.oneToOne([bob, carol, dave], amounts, { from: alice });
// 检查 3 个账户的 Noah 币数量
const bobBalance = await noahTokenInstance.balanceOf(bob);
const carolBalance = await noahTokenInstance.balanceOf(carol);
const daveBalance = await noahTokenInstance.balanceOf(dave);
assert.equal(bobBalance.toString(), amounts[0]);
assert.equal(carolBalance.toString(), amounts[1]);
assert.equal(daveBalance.toString(), amounts[2]);
// 检查空投合约的 Noah 币数量
const airdropBalance = await noahTokenInstance.balanceOf(airdropInstance.address);
assert.equal(airdropBalance.toString(), airdropTotalAmount - amounts.reduce((a, b) => a + b));
});
});
再来测试任何人都可以空投,但需要收取手续费的版本。和上面的主要区别是手续费的计算。
const NoahToken = artifacts.require("NoahToken");
const AirdropFree = artifacts.require("AirdropFree");
contract("AirdropFree", (accounts) => {
const [alice, bob, carol, dave] = accounts;
it("oneToMany", async () => {
// 发 Noah 币,发行 10240000 个
const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '10240000', { from: alice });
// 发空投合约 设置营销收款账户为 dave;手续费为万分之 10(0.001)
const airdropInstance = await AirdropFree.new(noahTokenInstance.address, dave, 10, { from: alice });
// 授权空投合约可以操作 10000 个 Noah 币
await noahTokenInstance.approve(airdropInstance.address, 10000, { from: alice });
// 给 2 个账户发空投,每个账户 1000 个 Noah 币
const amount = 1000;
await airdropInstance.oneToMany([bob, carol], amount, { from: alice });
// 检查 2 个账户的 Noah 币数量
const bobBalance = await noahTokenInstance.balanceOf(bob);
const carolBalance = await noahTokenInstance.balanceOf(carol);
assert.equal(bobBalance.toString(), amount, "bob balance is not 1000");
assert.equal(carolBalance.toString(), amount, "carol balance is not 1000");
const daveBalance = await noahTokenInstance.balanceOf(dave);
const fee = amount * 2 * 0.001// 手续费
// 检查 dave 的营销收款是否正确
assert.equal(daveBalance.toString(), fee, "dave balance is not 2");
const airdropTotalAmount = amount * 2 + fee;// 空投总费用
// 检查 alice 的 Noah 币数量
const aliceBalance = await noahTokenInstance.balanceOf(alice);
assert.equal(aliceBalance.toString(), 10240000 - airdropTotalAmount, "alice balance is not 10237998");
});
it("oneToOne", async () => {
// 发 Noah 币,发行 10240000 个
const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '10240000', { from: alice });
// 发空投合约
const airdropInstance = await AirdropFree.new(noahTokenInstance.address, dave, 10, { from: alice });
// 授权空投合约可以操作 10000 个 Noah 币
await noahTokenInstance.approve(airdropInstance.address, 10000, { from: alice });
// 给 2 个账户发空投,bob 1000 个,carol 2000 个
const amounts = [1000, 2000];
await airdropInstance.oneToOne([bob, carol], amounts, { from: alice });
// 检查 2 个账户的 Noah 币数量
const bobBalance = await noahTokenInstance.balanceOf(bob);
const carolBalance = await noahTokenInstance.balanceOf(carol);
assert.equal(bobBalance.toString(), amounts[0], "bob balance is not 10");
assert.equal(carolBalance.toString(), amounts[1], "carol balance is not 15");
const fee = amounts[0] * 0.001 + amounts[1] * 0.001// 手续费
// 检查 dave 的营销收款是否正确
const daveBalance = await noahTokenInstance.balanceOf(dave);
assert.equal(daveBalance.toString(), fee, "dave balance is not 3");
const airdropTotalAmount = amounts[0] + amounts[1] + fee;// 空投总费用
// 检查 alice 的 Noah 币数量
const aliceBalance = await noahTokenInstance.balanceOf(alice);
assert.equal(aliceBalance.toString(), 10240000 - airdropTotalAmount, "alice balance is not 10236997");
});
});
前端实现
一个数量对一堆地址的版本,我们可以使用一个 textarea 进行输入地址,每个换行符代表一个地址。
支持 Excel 与 txt 导入
当空投用户数量比较多时,在网页中输入的交互方式就捉襟见肘了。这时运营人员通常会使用 Excel 或者 txt 来统计数据。我们可以开发一个 Excel 与 txt 导入的功能。
不要小瞧这种体验功能,一个成功的 DApp 离不开这种细节。
首先安装一些库。
npm i react-dropzone xlsx formik react-icons
- react-dropzone 用来支持拖拽文件。
- xlsx 用来解析 Excel。
- formik 用来做表单控制和校验。
- react-icons 用来做图标库。
然后是实现具体的细节。
txt 的解析非常简单,我们自己操作即可。
Excel 的解析就需要依靠 xlsx 了。
首先实现一个导入组件。
function ImportExcel({ onImported }: { onImported: (data: any) => void }) {
const onDrop = useCallback((acceptedFiles: File[]) => {
const file = acceptedFiles[0];
if (file.type === "text/plain") {
const reader = new FileReader();
reader.onabort = () => console.log("file reading was aborted");
reader.onerror = () => console.log("file reading has failed");
reader.onload = () => {
const text = reader.result as string;
onImported(text);
};
reader.readAsText(file);
return;
}
const reader = new FileReader();
reader.onabort = () => console.log("file reading was aborted");
reader.onerror = () => console.log("file reading has failed");
reader.onload = () => {
const binaryStr = reader.result as string;
const wb = read(binaryStr, { type: "binary" });
const wsName = wb.SheetNames[0];
const ws = wb.Sheets[wsName];
const json = utils.sheet_to_json(ws);
onImported(json);
};
reader.readAsBinaryString(file);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: false,
maxSize: 1024 * 1024,
accept: {
"text/csv": [".cvs"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [ ".xlsx", ],
"application/vnd.ms-excel": [".xls"],
"text/plain": [".txt"],
},
});
return (
<div
{...getRootProps()}
className="p-4 border-2 border-gray-300 border-dashed rounded-sm cursor-pointer"
>
<input {...getInputProps()} />
{isDragActive ? (
<p>拖拽文件到这里</p>
) : (
<div>
<p>拖拽文件到这里,或者点击上传文件</p>
<p>支持的文件格式:.csv, .xlsx, .xls, .txt</p>
</div>
)}
</div>
);
}
再来实现一对多的组件,代码如下:
function OneToMany() {
const [isLoading, setIsLoading] = useState(false);
const onImported = (data: any) => {
try {
if (typeof data === "string") {
return data;
} else if (typeof data === "object") {
const addresses = data
.map(({ address }: any) => address)
.filter((address: string) => ethers.utils.isAddress(address))
.join("\n");
return addresses;
}
} catch (err) {
toast({
title: "导入失败",
status: "error",
});
return "";
}
};
const { data } = useSigner();
const nftContract = useContract({
...contract,
signerOrProvider: data,
});
const toast = useToast();
const airdrop = async (values: { addresses: string; amount: number }) => {
setIsLoading(true);
const addressesParam = values.addresses.trim().split("\n");
const isAddressValid = addressesParam.every((address) =>
ethers.utils.isAddress(address)
);
if (!isAddressValid) {
toast({
title: "地址不合法",
status: "error",
});
return;
}
try {
const { wait } = await nftContract?.oneToMany(
addressesParam,
values.amount
);
const receipt = await wait();
console.log(receipt, "receipt");
toast({
title: "空投成功",
status: "success",
isClosable: true,
});
} catch (err) {
console.log(err, "err");
toast({
title: "空投失败",
status: "error",
isClosable: true,
});
} finally {
setIsLoading(false);
}
};
return (
<div className="flex flex-col gap-2">
<Heading>多个地址使用相同的数量进行空投</Heading>
<Alert>
<div>
<div>支持文件导入。</div>
<ul>
<li>如果是 txt 类型的文件,需要将地址以换行符分隔。</li>
<li>
如果是表格类型的文件,需要将内容放在第一个 sheet
中,并将第一列命名为
address。同时保证地址为文本类型,而不是数字类型。
</li>
</ul>
</div>
</Alert>
<Formik
initialValues={{
addresses: "",
amount: 0,
}}
onSubmit={airdrop}
>
{({ errors, touched, handleSubmit, values, setValues }) => (
<form onSubmit={handleSubmit} className="flex flex-col gap-2">
<Field
as={ImportExcel}
id="excel"
name="excel"
onImported={(value: string) => {
setValues({
...values,
addresses: onImported(value),
});
}}
/>
<Alert>
<ul>
<li>每行代表一个地址</li>
</ul>
</Alert>
<FormControl isInvalid={!!errors.addresses && touched.addresses}>
<FormLabel htmlFor="addresses">地址</FormLabel>
<Field
as={Textarea}
id={"addresses"}
name="addresses"
placeholder="请输入要转账的地址"
validate={(value: string) => {
let error;
if (!value) {
error = "地址不能为空";
}
const addressesParam = value.trim().split("\n");
const isAddressValid = addressesParam.every((address) =>
ethers.utils.isAddress(address)
);
if (!isAddressValid) {
error = "地址格式不正确";
}
return error;
}}
></Field>
<FormErrorMessage>{errors.addresses}</FormErrorMessage>
</FormControl>
<FormControl isInvalid={!!errors.amount && touched.amount}>
<FormLabel htmlFor="amount">数量</FormLabel>
<Field
as={Input}
id={"amount"}
name="amount"
type="number"
min={0}
placeholder="请输入要转账的代币数量"
validate={(value: number) => {
let error;
if (value <= 0) {
error = "数量必须大于 0";
}
return error;
}}
></Field>
<FormErrorMessage>{errors.amount}</FormErrorMessage>
</FormControl>
<AirDropButton
isLoading={isLoading}
isDisabled={!values.addresses || !values.amount || !!errors}
/>
</form>
)}
</Formik>
</div>
);
}
效果如下:
地址与数量一对一的实现其实非常类似,代码如下:
function OneToOne() {
const [inputData, setInputData] = useState<
{ address: string; amount: number; isValid?: boolean }[]
>([]);
const [address, setAddress] = useState("");
const [amount, setAmount] = useState(0);
const addresses = inputData.map(({ address }) => address);
const amounts = inputData.map(({ amount }) => amount);
const isValid =
addresses.every((address) => ethers.utils.isAddress(address)) &&
amounts.every((amount) => amount > 0) &&
inputData.length > 0;
const { config } = usePrepareContractWrite({
...contract,
functionName: "oneToMany",
args: [addresses, amounts],
enabled: isValid,
});
const { write, data, isError } = useContractWrite(config);
const {
isSuccess,
isLoading,
isError: isWaitTransactionError,
} = useWaitForTransaction({
hash: data?.hash,
});
const toast = useToast();
useEffect(() => {
if (isWaitTransactionError) {
toast({
title: "空投失败",
status: "error",
isClosable: true,
});
}
if (isSuccess) {
toast({
title: "空投成功",
status: "success",
isClosable: true,
});
}
}, [isSuccess, isWaitTransactionError, toast]);
const onImported = (data: any) => {
if (typeof data === "string") {
const result = data.split("\n").map((item) => {
const [address, amount] = item.split(" ");
return {
address,
amount: Number(amount),
};
});
setInputData(result);
} else if (typeof data === "object") {
const result = data
.filter((item: any) => {
return (
item.address &&
item.amount &&
ethers.utils.isAddress(item.address) &&
Number(item.amount) > 0
);
})
.map((item: any) => ({
address: item.address,
amount: Number(item.amount),
}));
setInputData(result);
}
};
return (
<div className="flex flex-col gap-2">
<Heading>单个地址使用不同的数量进行空投</Heading>
<Alert>
<div>
<div>支持文件导入。</div>
<ul>
<li>
如果是 txt
类型的文件,需要将地址和数量以空格符分隔,每组数据以换行符分隔。
</li>
<li>
如果是表格类型的文件,需要将内容放在第一个 sheet
中,并将第一列命名为 address,第二列命名为
amount。同时保证地址为文本类型,而不是数字类型。
</li>
</ul>
</div>
</Alert>
<ImportExcel onImported={onImported} />
<Table>
<Thead>
<Tr>
<Th>地址</Th>
<Th className="w-28 md:w-40">数量</Th>
</Tr>
</Thead>
<Tbody>
{inputData.map(({ address, amount }, idx) => (
<Tr key={idx}>
<Td>
<Input
value={address}
onChange={(e) => {
const newData = [...inputData];
newData[idx].address = e.target.value;
setInputData(newData);
}}
borderColor={ethers.utils.isAddress(address) ? "" : "red.500"}
placeholder="请输入地址"
></Input>
</Td>
<Td>
<Input
value={amount}
onChange={(e) => {
const newData = [...inputData];
newData[idx].amount = Number(e.target.value);
setInputData(newData);
}}
borderColor={amount > 0 ? "" : "red.500"}
placeholder="请输入数量"
></Input>
</Td>
</Tr>
))}
<Tr>
<Td>
<Input
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder="请输入地址"
></Input>
</Td>
<Td>
<Input
type="number"
value={amount}
onChange={(e) => setAmount(Number(e.target.value))}
placeholder="请输入数量"
></Input>
</Td>
</Tr>
</Tbody>
</Table>
<div className="flex flex-col gap-2">
<Button
onClick={() => {
setInputData([...inputData, { address, amount }]);
}}
>
添加一列
</Button>
<Button
type="submit"
color={"white"}
bg={"pink.400"}
leftIcon={<Icon as={HiPaperAirplane} className="rotate-90" />}
isLoading={isLoading}
isDisabled={isLoading || !isValid}
onClick={() => write?.()}
>
发送
</Button>
</div>
</div>
);
}
效果如下:
线上体验地址:www.webnext.cloud/
Github 源码地址:github.com/luzhenqian/…
我们是一群立志改变世界的人。而 Web3 是未来世界一大变数,我们想帮助更多人了解并加入 Web3,如果你对 Web3 感兴趣,可以添加我的微信:LZQ20130415,邀你入群,一起沉淀、一起成长、一起拥抱未来。
今天的文章Web3年入百万的“空投”到底是什么?教你开发批量转账的空投DApp(涵盖前端、智能合约)分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/22158.html