diff --git a/docs/README.md b/docs/README.md
index e0cf1965..c1668bd3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1127,6 +1127,35 @@ donationRate = 10
```
+## token-id
+
+> **The ‘tokenId’ parameter defines the eToken that will be used in the button or widget.**
+
+?> The token ID parameter is optional. It accepts a string containing the token ID. Default value is null.
+
+
+**Example:**
+
+
+#### **HTML**
+
+```html
+token-id="c67bf5c2b6d91cfb46a5c1772582eff80d88686887be10aa63b0945479cf4ed4"
+```
+
+#### **JavaScript**
+
+```javascript
+tokenId: "c67bf5c2b6d91cfb46a5c1772582eff80d88686887be10aa63b0945479cf4ed4"
+```
+
+#### **React**
+
+```react
+tokenId = "c67bf5c2b6d91cfb46a5c1772582eff80d88686887be10aa63b0945479cf4ed4"
+```
+
+
# Contribute
PayButton is a community-driven open-source initiative. Contributions from the community are _crucial_ to the success of the project.
diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index 940e9fdb..814af78b 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -34,6 +34,8 @@
- [disable-sound](/?id=disable-sound)
- [size](/?id=size)
- [donation-rate](/?id=donation-rate)
+ - [token-id](/?id=token-id)
+
- [Contribute](/?id=contribute)
- [Developer Quick Start](/?id=developer-quick-start)
diff --git a/docs/zh-cn/README.md b/docs/zh-cn/README.md
index 2e5addaa..fedca865 100644
--- a/docs/zh-cn/README.md
+++ b/docs/zh-cn/README.md
@@ -1126,6 +1126,36 @@ donationRate = 10
```
+## token-id
+
+> **「tokenId」 参数用于定义在按钮或小组件中使用的 eToken。**
+
+?> tokenId 参数是可选的。它接受一个包含 token ID 的字符串。默认值为 null。
+
+
+**Example:**
+
+
+#### **HTML**
+
+```html
+token-id="c67bf5c2b6d91cfb46a5c1772582eff80d88686887be10aa63b0945479cf4ed4"
+```
+
+#### **JavaScript**
+
+```javascript
+tokenId: "c67bf5c2b6d91cfb46a5c1772582eff80d88686887be10aa63b0945479cf4ed4"
+```
+
+#### **React**
+
+```react
+tokenId = "c67bf5c2b6d91cfb46a5c1772582eff80d88686887be10aa63b0945479cf4ed4"
+```
+
+
+
# 贡献
diff --git a/docs/zh-cn/_sidebar.md b/docs/zh-cn/_sidebar.md
index 6acc45ce..12556353 100644
--- a/docs/zh-cn/_sidebar.md
+++ b/docs/zh-cn/_sidebar.md
@@ -33,6 +33,8 @@
- [disable-sound](/zh-cn/?id=disable-sound)
- [size](/zh-cn/?id=size)
- [donation-rate](/zh-cn/?id=donation-rate)
+ - [token-id](/zh-cn/?id=token-id)
+
- [贡献](/zh-cn/?id=贡献)
- [开发人员快速入门](/zh-cn/?id=开发人员快速入门)
- [入门](/zh-cn/?id=入门)
diff --git a/docs/zh-tw/README.md b/docs/zh-tw/README.md
index c93271f1..e65e4af5 100644
--- a/docs/zh-tw/README.md
+++ b/docs/zh-tw/README.md
@@ -1122,6 +1122,37 @@ donationRate = 10
```
+## token-id
+
+> **「tokenId」 參數用於定義在按鈕或小工具中使用的 eToken。**
+
+?> tokenId 參數為選填。它接受一個包含 token ID 的字串。預設值為 null。
+
+
+**Example:**
+
+
+#### **HTML**
+
+```html
+token-id="c67bf5c2b6d91cfb46a5c1772582eff80d88686887be10aa63b0945479cf4ed4"
+```
+
+#### **JavaScript**
+
+```javascript
+tokenId: "c67bf5c2b6d91cfb46a5c1772582eff80d88686887be10aa63b0945479cf4ed4"
+```
+
+#### **React**
+
+```react
+tokenId = "c67bf5c2b6d91cfb46a5c1772582eff80d88686887be10aa63b0945479cf4ed4"
+```
+
+
+
+
# 貢獻
diff --git a/docs/zh-tw/_sidebar.md b/docs/zh-tw/_sidebar.md
index 8bda09ed..39ceda84 100644
--- a/docs/zh-tw/_sidebar.md
+++ b/docs/zh-tw/_sidebar.md
@@ -32,6 +32,9 @@
- [auto-close](/zh-tw/?id=auto-close)
- [disable-sound](/zh-tw/?id=disable-sound)
- [size](/zh-tw/?id=size)
+ - [donation-rate](/zh-tw/?id=donation-rate)
+ - [token-id](/zh-tw/?id=token-id)
+
- [貢獻](/zh-tw/?id=貢獻)
- [開發人員快速入門](/zh-tw/?id=開發人員快速入門)
- [入門](/zh-tw/?id=入門)
diff --git a/paybutton/src/index.tsx b/paybutton/src/index.tsx
index d6d911bc..56a056c7 100644
--- a/paybutton/src/index.tsx
+++ b/paybutton/src/index.tsx
@@ -106,6 +106,7 @@ const allowedProps = [
'transactionText',
'size',
'donationRate',
+ 'tokenId'
];
const requiredProps = [
diff --git a/react/lib/components/PayButton/PayButton.tsx b/react/lib/components/PayButton/PayButton.tsx
index f8c83981..be7684f8 100644
--- a/react/lib/components/PayButton/PayButton.tsx
+++ b/react/lib/components/PayButton/PayButton.tsx
@@ -59,6 +59,7 @@ export interface PayButtonProps extends ButtonProps {
sizeScaleAlreadyApplied?: boolean;
donationAddress?: string;
donationRate?: number;
+ tokenId?: string;
}
export const PayButton = ({
@@ -92,7 +93,8 @@ export const PayButton = ({
size = 'md',
sizeScaleAlreadyApplied = false,
donationRate = DEFAULT_DONATION_RATE,
- donationAddress = config.donationAddress
+ donationAddress = config.donationAddress,
+ tokenId,
}: PayButtonProps): React.ReactElement => {
const [dialogOpen, setDialogOpen] = useState(false);
const [disabled, setDisabled] = useState(false);
@@ -310,7 +312,8 @@ export const PayButton = ({
expectedPaymentId: paymentId,
currencyObj,
donationRate
- }
+ },
+ tokenId,
})
}
if (altpaymentSocket === undefined && useAltpayment) {
@@ -466,6 +469,7 @@ export const PayButton = ({
donationRate={donationRate}
convertedCurrencyObj={convertedCurrencyObj}
setConvertedCurrencyObj={setConvertedCurrencyObj}
+ tokenId={tokenId}
/>
{errorMsg && (
{
const [success, setSuccess] = useState(false);
const [internalDisabled, setInternalDisabled] = useState(false);
@@ -259,6 +261,7 @@ export const PaymentDialog = ({
donationRate={donationRate}
convertedCurrencyObj={convertedCurrencyObj}
setConvertedCurrencyObj={setConvertedCurrencyObj}
+ tokenId={tokenId}
foot={success && (
= props => {
donationRate = DEFAULT_DONATION_RATE,
setConvertedCurrencyObj = () => {},
setPaymentId,
+ tokenId,
} = props;
const [loading, setLoading] = useState(true);
const [draftAmount, setDraftAmount] = useState("")
@@ -331,6 +334,7 @@ export const Widget: React.FunctionComponent = props => {
const [thisAmount, setThisAmount] = useState(props.amount)
const [thisCurrencyObject, setThisCurrencyObject] = useState(props.currencyObject)
+ const [tokenName, setTokenName] = useState(null)
const blurCSS = isPropsTrue(disabled) ? { filter: 'blur(5px)' } : {}
// inject keyframes once (replacement for @global in makeStyles)
@@ -500,6 +504,10 @@ export const Widget: React.FunctionComponent = props => {
)}' stroke='%23fff' stroke-width='.6'/%3E%3Cpath d='m7.2979 14.697-2.6964-2.6966 0.89292-0.8934c0.49111-0.49137 0.90364-0.88958 0.91675-0.88491 0.013104 0.0047 0.71923 0.69866 1.5692 1.5422 0.84994 0.84354 1.6548 1.6397 1.7886 1.7692l0.24322 0.23547 7.5834-7.5832 1.8033 1.8033-9.4045 9.4045z' fill='%23fff' stroke-width='.033708'/%3E%3C/svg%3E%0A`
}, [theme])
+ const getTokenIconUrl = useCallback((tokenId: string): string => {
+ return `https://icons.etokens.cash/128/${tokenId}.png`
+ }, [])
+
useEffect(() => {
if (thisCurrencyObject?.string !== undefined) {
const raw = stripFormatting(thisCurrencyObject.string);
@@ -541,6 +549,7 @@ export const Widget: React.FunctionComponent = props => {
wsBaseUrl,
setTxsSocket: setThisTxsSocket,
setNewTxs: setThisNewTxs,
+ tokenId,
})
if (thisUseAltpayment) {
await setupAltpaymentSocket({
@@ -589,6 +598,26 @@ export const Widget: React.FunctionComponent = props => {
})()
}, [thisNewTxs, to, apiBaseUrl])
+ useEffect(() => {
+ ;(async (): Promise => {
+ if (tokenId && tokenId !== '') {
+ try {
+ const tokenInfo = await getTokenInfo(tokenId, to)
+ const name = tokenInfo.genesisInfo.tokenTicker ?? null
+ setTokenName(name)
+ } catch (err) {
+ console.error('Failed to fetch token info:', err)
+ setTokenName(null)
+ setErrorMsg('Unable to load token information')
+ } finally {
+ setLoading(false)
+ }
+ return
+ }
+ setLoading(false)
+ })()
+ }, [tokenId, to])
+
useEffect(() => {
if (
isChild ||
@@ -805,7 +834,7 @@ export const Widget: React.FunctionComponent = props => {
setText(
`Send ${amountToDisplay} ${thisCurrencyObject.currency} = ${convertedAmountToDisplay} ${thisAddressType}`,
)
- const url = resolveUrl(thisAddressType, convertedObj.float)
+ const url = resolveUrl(thisAddressType, convertedObj.float, tokenId)
setUrl(url ?? "")
}
} else {
@@ -830,16 +859,16 @@ export const Widget: React.FunctionComponent = props => {
amountToDisplay = amountWithDonationObj.string
}
- setText(`Send ${amountToDisplay} ${cur}`)
+ setText(`Send ${amountToDisplay} ${tokenId ? tokenName : cur}`)
// Pass base amount (without donation) to resolveUrl
- nextUrl = resolveUrl(cur, baseAmount)
+ nextUrl = resolveUrl(cur, baseAmount, tokenId)
} else {
- setText(`Send any amount of ${thisAddressType}`)
- nextUrl = resolveUrl(thisAddressType)
+ setText(`Send any amount of ${tokenId ? tokenName : thisAddressType}`)
+ nextUrl = resolveUrl(thisAddressType, undefined, tokenId)
}
setUrl(nextUrl ?? '')
}
- }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType, shouldApplyDonation])
+ }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType, shouldApplyDonation, tokenName, tokenId])
useEffect(() => {
try {
@@ -961,7 +990,7 @@ export const Widget: React.FunctionComponent = props => {
setRecentlyCopied(true)
}, [disabled, to, url, setCopied, setRecentlyCopied, qrLoading])
- const resolveUrl = useCallback((currency: string, amount?: number) => {
+ const resolveUrl = useCallback((currency: string, amount?: number, tokenId?: string) => {
if (disabled || !to) return;
const prefix = CURRENCY_PREFIXES_MAP[currency.toLowerCase() as typeof CRYPTO_CURRENCIES[number]];
@@ -978,11 +1007,18 @@ export const Widget: React.FunctionComponent = props => {
const donationPercent = userDonationRate / 100
// Calculate donation amount from base amount
const thisDonationAmount = amount * donationPercent
-
- thisUrl += `?amount=${amount}`
- thisUrl += `&addr=${donationAddress}&amount=${thisDonationAmount.toFixed(decimals)}`;
+ if (tokenId) {
+ thisUrl += `?token_decimalized_qty=${amount}`
+ } else {
+ thisUrl += `?amount=${amount}`
+ thisUrl += `&addr=${donationAddress}&amount=${thisDonationAmount.toFixed(decimals)}`;
+ }
} else {
- thisUrl += `?amount=${amount}`
+ if (tokenId) {
+ thisUrl += `?token_decimalized_qty=${amount}`
+ } else {
+ thisUrl += `?amount=${amount}`
+ }
}
}
@@ -991,9 +1027,14 @@ export const Widget: React.FunctionComponent = props => {
thisUrl += `${separator}op_return_raw=${opReturn}`;
}
+ if (tokenId) {
+ const separator = thisUrl.includes('?') ? '&' : '?';
+ thisUrl += `${separator}token_id=${tokenId}`;
+ }
+
return thisUrl;
},
- [disabled, to, opReturn, userDonationRate, donationAddress, donationEnabled, shouldApplyDonation]
+ [disabled, to, opReturn, userDonationRate, donationAddress, donationEnabled, shouldApplyDonation, tokenId]
)
const stripFormatting = (s: string) => {
return s.replace(/,/g, '').replace(/(\.\d*?[1-9])0+$/, '$1').replace(/\.0+$/, '');
@@ -1033,6 +1074,7 @@ export const Widget: React.FunctionComponent = props => {
}
}
+
const qrCode = (
= props => {
bgColor={isDarkMode ? '#1a1a1a' : '#ffffff'}
fgColor={theme.palette.tertiary as unknown as string}
imageSettings={{
- src: success ? checkSvg : isValidCashAddress(to) ? bchSvg : xecSvg,
+ src: success ? checkSvg : tokenId ? getTokenIconUrl(tokenId) : isValidCashAddress(to) ? bchSvg : xecSvg,
excavate: false,
height: 112,
width: 112,
diff --git a/react/lib/util/chronik.ts b/react/lib/util/chronik.ts
index 7714d257..8ae51662 100644
--- a/react/lib/util/chronik.ts
+++ b/react/lib/util/chronik.ts
@@ -113,6 +113,27 @@ export async function satoshisToUnit(satoshis: bigint, networkFormat: string): P
throw new Error('[CHRONIK]: Invalid address')
}
+const getTokenAmount = async (transaction: Tx, tokenId: string, address: string, networkSlug: string): Promise => {
+ let totalTokenOutput = BigInt(0);
+ const tokenInfo = await getTokenInfo(tokenId, address);
+ const decimals = tokenInfo.genesisInfo.decimals;
+ const divisor = 10 ** decimals;
+
+ for (const output of transaction.outputs) {
+ if (output.token?.tokenId === tokenId) {
+ const outputAddress = outputScriptToAddress(networkSlug, output.outputScript)
+
+ if(outputAddress === address) {
+ const atoms = BigInt(output.token.atoms);
+
+ totalTokenOutput += atoms / BigInt(divisor);
+ }
+ }
+ }
+
+ return totalTokenOutput.toString();
+}
+
const getTransactionAmountAndData = async (transaction: Tx, addressString: string): Promise<{amount: string, opReturn: string}> => {
let totalOutput = BigInt(0);
let totalInput = BigInt(0);
@@ -145,14 +166,15 @@ const getTransactionAmountAndData = async (transaction: Tx, addressString: stri
}
}
-const getTransactionFromChronikTransaction = async (transaction: Tx, address: string): Promise => {
+const getTransactionFromChronikTransaction = async (transaction: Tx, address: string, tokenId?: string): Promise => {
const { amount, opReturn } = await getTransactionAmountAndData(transaction, address)
const parsedOpReturn = resolveOpReturn(opReturn)
const networkSlug = getAddressPrefix(address)
const inputAddresses = getSortedInputAddresses(networkSlug, transaction)
+ const tokenAmount = tokenId ? await getTokenAmount(transaction, tokenId, address, networkSlug) : undefined;
return {
hash: transaction.txid,
- amount,
+ amount: tokenId ? tokenAmount! : amount,
address,
timestamp: transaction.block !== undefined ? transaction.block.timestamp : transaction.timeFirstSeen,
confirmed: transaction.block !== undefined,
@@ -236,12 +258,25 @@ function getSortedInputAddresses (networkSlug: string, transaction: Tx): string[
return sortedInputAddresses
}
+export const getTokenInfo = async (tokenId: string, address:string) => {
+ const networkSlug = getAddressPrefix(address)
+ const blockchainUrls = config.networkBlockchainURLs[networkSlug];
+
+ const chronik = await ChronikClient.useStrategy(
+ ConnectionStrategy.AsOrdered,
+ blockchainUrls,
+ );
+ const tokenInfo = await chronik.token(tokenId);
+
+ return tokenInfo;
+}
export const parseWebsocketMessage = async (
wsMsg: any,
setNewTx: Function,
chronik: ChronikClient,
- address: string
+ address: string,
+ tokenId?: string
): Promise => {
const { type } = wsMsg;
if (type === 'Error') {
@@ -252,8 +287,7 @@ export const parseWebsocketMessage = async (
case 'TX_ADDED_TO_MEMPOOL': {
const rawTransaction = await chronik.tx(wsMsg.txid);
- const transaction = await getTransactionFromChronikTransaction(rawTransaction, address ?? '')
-
+ const transaction = await getTransactionFromChronikTransaction(rawTransaction, address ?? '', tokenId)
setNewTx([transaction]);
break;
}
@@ -264,7 +298,8 @@ export const parseWebsocketMessage = async (
export const initializeChronikWebsocket = async (
address: string,
- setNewTx: Function
+ setNewTx: Function,
+ tokenId?: string
): Promise => {
const networkSlug = getAddressPrefix(address)
const blockchainUrls = config.networkBlockchainURLs[networkSlug];
@@ -279,7 +314,8 @@ export const initializeChronikWebsocket = async (
msg,
setNewTx,
chronik,
- address
+ address,
+ tokenId
);
},
});
diff --git a/react/lib/util/socket.ts b/react/lib/util/socket.ts
index 2961ff3b..7ebf4886 100644
--- a/react/lib/util/socket.ts
+++ b/react/lib/util/socket.ts
@@ -122,6 +122,7 @@ interface SetupTxsSocketParams {
setNewTxs: Function
setDialogOpen?: Function
checkSuccessInfo?: CheckSuccessInfo
+ tokenId?: string
}
export const setupTxsSocket = async (params: SetupTxsSocketParams): Promise => {
@@ -145,9 +146,13 @@ export const setupChronikWebSocket = async (params: SetupTxsSocketParams): Promi
params.setTxsSocket(undefined);
}
- const newChronikSocket = await initializeChronikWebsocket(params.address, (transactions: Transaction[]) => {
- params.setNewTxs(transactions);
- });
+ const newChronikSocket = await initializeChronikWebsocket(
+ params.address,
+ (transactions: Transaction[]) => {
+ params.setNewTxs(transactions);
+ },
+ params.tokenId
+ );
params.setTxsSocket(newChronikSocket);
}