Skip to content

Commit

Permalink
feat: Payment In USDT (#1272)
Browse files Browse the repository at this point in the history
* feat: Payment In USDT

* fix: add translate

* fix: Flex to Space

* fix: EvmPayButtons -> EvmPayButton

* fix: Add chainSelect argument

* Update .changeset/kind-eels-press.md

* Update docs/guide/payment-in-usdt.zh-CN.md

Co-authored-by: thinkasany <[email protected]>

* Update docs/guide/payment-in-usdt.zh-CN.md

Co-authored-by: thinkasany <[email protected]>

---------

Co-authored-by: 愚指导 <[email protected]>
Co-authored-by: thinkasany <[email protected]>
  • Loading branch information
3 people authored Dec 24, 2024
1 parent 703f16a commit 2ee466c
Show file tree
Hide file tree
Showing 13 changed files with 407 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/kind-eels-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ant-design/web3': minor
---

feat: Add chainSelect argument
106 changes: 106 additions & 0 deletions docs/guide/demos/best-practice/components/pay-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// /components/pay-button.tsx
import React from 'react';
import { ConnectButton, Connector } from '@ant-design/web3';
import { MetaMask, WagmiWeb3ConfigProvider } from '@ant-design/web3-wagmi';
import { Button, Space } from 'antd';
import { createConfig, http } from 'wagmi';
import { arbitrum, mainnet, optimism, polygon } from 'wagmi/chains';
import { injected } from 'wagmi/connectors';

import EvmSignTransaction from './send';

type PayButtonsProps = {
setTokenEcosystem?: (token: string) => void;
tokenEcosystem: string;
signCallback: (signTransfer: (toAddress: string, amount: number) => void) => void;
payCallback: (signTransfer: string, address: string) => void;
onRejectSwitchChain?: (id: number) => void;
};

export const EvmPayButton: React.FC<PayButtonsProps> = ({
setTokenEcosystem,
tokenEcosystem,
signCallback,
payCallback,
onRejectSwitchChain,
}) => {
const config = createConfig({
chains: [mainnet, polygon, arbitrum, optimism],
transports: {
[mainnet.id]: http(),
[polygon.id]: http(),
[arbitrum.id]: http(),
[optimism.id]: http(),
},
connectors: [
injected({
target: 'metaMask',
}),
],
});

return (
<div>
<WagmiWeb3ConfigProvider
config={config}
eip6963={{
autoAddInjectedWallets: true,
}}
wallets={[MetaMask()]}
chains={[mainnet, polygon, arbitrum, optimism]}
>
<Space size="middle">
<Connector
modalProps={{
footer: (
<>
Powered by{' '}
<a href="https://web3.ant.design/" target="_blank" rel="noreferrer">
Ant Design Web3
</a>
</>
),
}}
>
<ConnectButton chainSelect={false} />
</Connector>
<Connector
modalProps={{
footer: (
<>
Powered by{' '}
<a href="https://web3.ant.design/" target="_blank" rel="noreferrer">
Ant Design Web3
</a>
</>
),
}}
>
<ConnectButton chainSelect={false} />
</Connector>
<EvmSignTransaction
setTokenEcosystem={setTokenEcosystem}
tokenEcosystem={tokenEcosystem}
signTransaction={(signTransfer, address) => {
payCallback(signTransfer, address);
}}
onRejectSwitchChain={onRejectSwitchChain}
renderSignButton={(signTransfer, disabled, signLoading) => (
<Button
type="primary"
style={{ width: 200 }}
loading={signLoading}
disabled={disabled}
onClick={() => {
signCallback(signTransfer);
}}
>
Pay
</Button>
)}
/>
</Space>
</WagmiWeb3ConfigProvider>
</div>
);
};
35 changes: 35 additions & 0 deletions docs/guide/demos/best-practice/components/select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// /components/select.tsx
import React from 'react';
import { Radio, Space } from 'antd';

import { TOKEN_PAY_ADDRESS } from '../constants/tokenPayAddress';

interface ChainSelectGroupProps {
ecosystem: string;
onChange: (ecosystem: string) => void;
}

const ChainSelect: React.FC<ChainSelectGroupProps> = (props) => {
const chainList = TOKEN_PAY_ADDRESS.chains;

return (
<Space size="middle">
<Radio.Group
onChange={(e) => {
props.onChange(e.target.value);
}}
value={props.ecosystem}
>
{chainList.map((info) => {
return (
<Radio key={info.ecosystem} value={info.ecosystem}>
{info.name}
</Radio>
);
})}
</Radio.Group>
</Space>
);
};

export default ChainSelect;
106 changes: 106 additions & 0 deletions docs/guide/demos/best-practice/components/send.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// /components/send.tsx
import React, { useEffect, useState } from 'react';
import { useAccount } from '@ant-design/web3';
import { parseAbi, parseUnits } from 'viem';
import { useChainId, useSwitchChain, useWriteContract } from 'wagmi';

import { TOKEN_PAY_ADDRESS } from '../constants/tokenPayAddress';

type SignTransactionProps = {
setTokenEcosystem?: (token: string) => void;
tokenEcosystem: string;
signTransaction: (signTransfer: string, address: string) => void;
renderSignButton: (
signTransfer: (toAddress: string, amount: number) => void,
disabled: boolean,
loading: boolean,
) => React.ReactNode;
onRejectSwitchChain?: (id: number) => void;
};

const EvmSignTransaction: React.FC<SignTransactionProps> = ({
setTokenEcosystem,
tokenEcosystem,
signTransaction,
renderSignButton,
onRejectSwitchChain,
}) => {
const [signLoading, setSignLoading] = useState<boolean>(false);
const { writeContractAsync } = useWriteContract();
const { switchChain } = useSwitchChain();
const chainId = useChainId();
const { account } = useAccount();

useEffect(() => {
if (account?.address) {
const chainList = TOKEN_PAY_ADDRESS.chains;
const changeChainId = chainList.find((item) => item.ecosystem === tokenEcosystem)?.id;
if (changeChainId && changeChainId !== chainId) {
switchChain?.(
{ chainId: changeChainId },
{
onError: (error) => {
if (error.message.includes('User rejected')) {
onRejectSwitchChain?.(chainId);
}
},
},
);
}
}
}, [tokenEcosystem, account]);

useEffect(() => {
if (chainId && !tokenEcosystem) {
const chainList = TOKEN_PAY_ADDRESS.chains;
const initTokenEcosystem = chainList.find((item) => item.id === chainId)?.ecosystem;
if (initTokenEcosystem && account) {
setTokenEcosystem?.(initTokenEcosystem);
} else {
setTokenEcosystem?.(chainList[0].ecosystem);
}
}
}, [account]);

const signTransfer = async (toAddress: string, amount: number) => {
try {
setSignLoading(true);
// transfer ABI
// {
// "constant": false,
// "inputs": [
// { "name": "_to", "type": "address" },
// { "name": "_value", "type": "uint256" }
// ],
// "name": "transfer",
// "outputs": [],
// "payable": false,
// "stateMutability": "nonpayable",
// "type": "function"
// },
const decimals = TOKEN_PAY_ADDRESS.chains.find(
(item) => item.ecosystem === tokenEcosystem,
)?.decimals;
const contractAddress = TOKEN_PAY_ADDRESS.chains.find(
(item) => item.ecosystem === tokenEcosystem,
)?.address;
const signTransferHash = await writeContractAsync({
abi: parseAbi(['function transfer(address _to, uint256 _value)']),
address: contractAddress as `0x${string}`,
functionName: 'transfer',
args: [
toAddress.toLocaleLowerCase() as `0x${string}`,
parseUnits(amount.toString(), decimals!),
],
});
setSignLoading(false);
signTransaction?.(signTransferHash, account?.address || '');
} catch (error) {
console.log('error', (error as any).message);
setSignLoading(false);
}
};

return <div>{renderSignButton(signTransfer, !account, signLoading)}</div>;
};
export default EvmSignTransaction;
58 changes: 58 additions & 0 deletions docs/guide/demos/best-practice/constants/tokenPayAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// /constants/tokenPayAddress.ts
interface TokenInfo {
name: string;
icon: string;
symbol: string;
chains: {
name: string;
id?: number;
decimals: number;
ecosystem: string;
address: string;
txScan?: string;
network?: string;
}[];
}
export const TOKEN_PAY_ADDRESS: TokenInfo = {
name: 'USDT',
icon: 'https://mdn.alipayobjects.com/huamei_hsbbrh/afts/img/A*HkpaQoYlReEAAAAAAAAAAAAADiOMAQ/original',
symbol: 'usdt',
chains: [
{
name: 'Ethereum',
id: 1,
decimals: 6,
ecosystem: 'ethereum',
network: 'mainnet',
txScan: 'https://etherscan.io/tx/',
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
},
{
name: 'Polygon',
id: 137,
decimals: 6,
ecosystem: 'polygon',
network: 'polygon',
txScan: 'https://polygonscan.com/tx/',
address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f',
},
{
name: 'Arbitrum',
id: 42161,
decimals: 6,
ecosystem: 'arbitrum',
network: 'arbitrum',
txScan: 'https://arbiscan.io/tx/',
address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
},
{
name: 'Optimism',
id: 10,
decimals: 6,
ecosystem: 'optimism',
network: 'optimism',
txScan: 'https://optimistic.etherscan.io/tx/',
address: '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58',
},
],
};
46 changes: 46 additions & 0 deletions docs/guide/demos/best-practice/usdt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// /usdt.tsx
import React, { useState } from 'react';
import { Card, message, Space, Typography } from 'antd';

import { EvmPayButton } from './components/pay-button';
import ChainSelect from './components/select';
import { TOKEN_PAY_ADDRESS } from './constants/tokenPayAddress';

const { Title } = Typography;

const PaymentInUSDT: React.FC = () => {
// token ecosystem
const [tokenEcosystem, setTokenEcosystem] = useState<string>('');

const onSubmitCashier = async (sign: (toAddress: string, amount: number) => void) => {
// The address and amount are obtained from the backend service
sign('0x35ceCD3d51Fe9E5AD14ea001475668C5A5e5ea76', 10);
};

const runPay = async (sign: string, address: string) => {
message.success('Pay success');
};

return (
<Card title="Payment in USDT">
<Space direction="vertical" size="middle">
<Title level={3}>Select Chain</Title>
<ChainSelect ecosystem={tokenEcosystem} onChange={setTokenEcosystem} />
<EvmPayButton
setTokenEcosystem={setTokenEcosystem}
tokenEcosystem={tokenEcosystem}
signCallback={onSubmitCashier}
payCallback={runPay}
onRejectSwitchChain={(id) => {
const oldTokenEcosystem = TOKEN_PAY_ADDRESS.chains.find(
(item) => item.id === id,
)?.ecosystem;
setTokenEcosystem(oldTokenEcosystem || '');
}}
/>
</Space>
</Card>
);
};

export default PaymentInUSDT;
14 changes: 14 additions & 0 deletions docs/guide/payment-in-usdt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
nav: Payment In USDT
group: Best Practice
---

# Payment In USDT

When your project needs to support `USDT` payments and needs to support `USDT/USDC` on multiple chains, the following can help you.

You can use our official adapter with `@ant-design/web3` to quickly connect to various blockchains to support `USDT/USDC` payments on these chains at the same time.

You can do this:

<code compact src="./demos/best-practice/usdt.tsx"></code>
14 changes: 14 additions & 0 deletions docs/guide/payment-in-usdt.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
nav: 用 USDT 支付
group: 最佳实践
---

# 用 USDT 支付

当你的项目需要支持 `USDT` 付款的时候,并且需要支持多个链的 `USDT` / `USDC` 时,下边这些可以帮到你。

你可以通过我们官方提供的适配器配合 `@ant-design/web3` 使用,快速连接各类区块链,以便于同时支持这些链的 `USDT` / `USDC` 支付。

你可以这样做:

<code compact src="./demos/best-practice/usdt.tsx"></code>
Loading

0 comments on commit 2ee466c

Please sign in to comment.