Skip to content

Commit 7f86530

Browse files
committed
update
1 parent 46c3abb commit 7f86530

File tree

7 files changed

+400
-0
lines changed

7 files changed

+400
-0
lines changed

.changeset/stupid-buses-wink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Add 2 new Pay functions: convertFiatToCrypto and convertCryptoToFiat

packages/thirdweb/src/exports/pay.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,13 @@ export type {
6666
PayTokenInfo,
6767
PayOnChainTransactionDetails,
6868
} from "../pay/utils/commonTypes.js";
69+
70+
export {
71+
convertFiatToCrypto,
72+
type ConvertFiatToCryptoParams,
73+
} from "../pay/convert/fiatToCrypto.js";
74+
75+
export {
76+
convertCryptoToFiat,
77+
type ConvertCryptoToFiatParams,
78+
} from "../pay/convert/cryptoToFiat.js";
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { describe, expect, it } from "vitest";
2+
import { TEST_CLIENT } from "~test/test-clients.js";
3+
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
4+
import { base } from "../../chains/chain-definitions/base.js";
5+
import { ethereum } from "../../chains/chain-definitions/ethereum.js";
6+
import { sepolia } from "../../chains/chain-definitions/sepolia.js";
7+
import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js";
8+
import { convertCryptoToFiat } from "./cryptoToFiat.js";
9+
10+
describe.runIf(process.env.TW_SECRET_KEY)("Pay: crypto-to-fiat", () => {
11+
it("should convert ETH price to USD on Ethereum mainnet", async () => {
12+
const data = await convertCryptoToFiat({
13+
chain: ethereum,
14+
fromTokenAddress: NATIVE_TOKEN_ADDRESS,
15+
fromAmount: 1,
16+
to: "usd",
17+
client: TEST_CLIENT,
18+
});
19+
expect(data.result).toBeDefined();
20+
// Should be a number
21+
expect(!Number.isNaN(data.result)).toBe(true);
22+
// Since eth is around US$3000, we can add a test to check if the price is greater than $1500 (as a safe margin)
23+
// let's hope that scenario does not happen :(
24+
expect(Number(data.result) > 1500).toBe(true);
25+
});
26+
27+
it("should convert ETH price to USD on Base mainnet", async () => {
28+
const data = await convertCryptoToFiat({
29+
chain: base,
30+
fromTokenAddress: NATIVE_TOKEN_ADDRESS,
31+
fromAmount: 1,
32+
to: "usd",
33+
client: TEST_CLIENT,
34+
});
35+
expect(data.result).toBeDefined();
36+
// Should be a number
37+
expect(!Number.isNaN(data.result)).toBe(true);
38+
// Since eth is around US$3000, we can add a test to check if the price is greater than $1500 (as a safe margin)
39+
// let's hope that scenario does not happen :(
40+
expect(data.result > 1500).toBe(true);
41+
});
42+
43+
it("should return zero if fromAmount is zero", async () => {
44+
const data = await convertCryptoToFiat({
45+
chain: base,
46+
fromTokenAddress: NATIVE_TOKEN_ADDRESS,
47+
fromAmount: 0,
48+
to: "usd",
49+
client: TEST_CLIENT,
50+
});
51+
expect(data.result).toBe(0);
52+
});
53+
54+
it("should throw error for testnet chain (because testnets are not supported", async () => {
55+
await expect(() =>
56+
convertCryptoToFiat({
57+
chain: sepolia,
58+
fromTokenAddress: NATIVE_TOKEN_ADDRESS,
59+
fromAmount: 1,
60+
to: "usd",
61+
client: TEST_CLIENT,
62+
}),
63+
).rejects.toThrowError(
64+
`Cannot fetch price for a testnet (chainId: ${sepolia.id})`,
65+
);
66+
});
67+
68+
it("should throw error if fromTokenAddress is set to an invalid EVM address", async () => {
69+
await expect(() =>
70+
convertCryptoToFiat({
71+
chain: ethereum,
72+
fromTokenAddress: "haha",
73+
fromAmount: 1,
74+
to: "usd",
75+
client: TEST_CLIENT,
76+
}),
77+
).rejects.toThrowError(
78+
"Invalid fromTokenAddress. Expected a valid EVM contract address",
79+
);
80+
});
81+
82+
it("should throw error if fromTokenAddress is set to a wallet address", async () => {
83+
await expect(() =>
84+
convertCryptoToFiat({
85+
chain: base,
86+
fromTokenAddress: TEST_ACCOUNT_A.address,
87+
fromAmount: 1,
88+
to: "usd",
89+
client: TEST_CLIENT,
90+
}),
91+
).rejects.toThrowError(
92+
`Error: ${TEST_ACCOUNT_A.address} on chainId: ${base.id} is not a valid contract address.`,
93+
);
94+
});
95+
});
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { Address } from "abitype";
2+
import type { Chain } from "../../chains/types.js";
3+
import type { ThirdwebClient } from "../../client/client.js";
4+
import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js";
5+
import { getBytecode } from "../../contract/actions/get-bytecode.js";
6+
import { getContract } from "../../contract/contract.js";
7+
import { isAddress } from "../../utils/address.js";
8+
import { getClientFetch } from "../../utils/fetch.js";
9+
import { getPayConvertCryptoToFiatEndpoint } from "../utils/definitions.js";
10+
11+
/**
12+
* Props for the `convertCryptoToFiat` function
13+
* @buyCrypto
14+
*/
15+
export type ConvertCryptoToFiatParams = {
16+
client: ThirdwebClient;
17+
/**
18+
* The contract address of the token
19+
* For native token, use NATIVE_TOKEN_ADDRESS
20+
*/
21+
fromTokenAddress: Address;
22+
/**
23+
* The amount of token to convert to fiat value
24+
*/
25+
fromAmount: number;
26+
/**
27+
* The chain that the token is deployed to
28+
*/
29+
chain: Chain;
30+
/**
31+
* The fiat symbol. e.g "usd"
32+
* Only USD is supported at the moment.
33+
*/
34+
to: "usd";
35+
};
36+
37+
/**
38+
* Get a price of a token (using tokenAddress + chainId) in fiat.
39+
* Only USD is supported at the moment.
40+
* @example
41+
* ### Basic usage
42+
* For native token (non-ERC20), you should use NATIVE_TOKEN_ADDRESS as the value for `tokenAddress`
43+
* ```ts
44+
* import { convertCryptoToFiat } from "thirdweb/pay";
45+
*
46+
* // Get Ethereum price
47+
* const result = convertCryptoToFiat({
48+
* fromTokenAddress: NATIVE_TOKEN_ADDRESS,
49+
* // This is not case sensitive, so either "USD" or "usd" is fine
50+
* to: "USD",
51+
* chain: ethereum,
52+
* fromAmount: 1,
53+
* });
54+
*
55+
* // Result: 3404.11
56+
* ```
57+
* @buyCrypto
58+
* @returns a number representing the price (in selected fiat) of "x" token, with "x" being the `fromAmount`.
59+
*/
60+
export async function convertCryptoToFiat(
61+
options: ConvertCryptoToFiatParams,
62+
): Promise<{ result: number }> {
63+
const { client, fromTokenAddress, to, chain, fromAmount } = options;
64+
if (Number(fromAmount) === 0) {
65+
return { result: 0 };
66+
}
67+
try {
68+
// Testnets just don't work with our current provider(s)
69+
if (chain.testnet === true) {
70+
throw new Error(
71+
`Cannot fetch price for a testnet (chainId: ${chain.id})`,
72+
);
73+
}
74+
// Some provider that we are using will return `0` for unsupported token
75+
// so we should do some basic input validations before sending the request
76+
77+
// Make sure it's a valid EVM address
78+
if (!isAddress(fromTokenAddress)) {
79+
throw new Error(
80+
"Invalid fromTokenAddress. Expected a valid EVM contract address",
81+
);
82+
}
83+
// Make sure it's either a valid contract or a native token address
84+
if (fromTokenAddress.toLowerCase() !== NATIVE_TOKEN_ADDRESS.toLowerCase()) {
85+
const bytecode = await getBytecode(
86+
getContract({
87+
address: fromTokenAddress,
88+
chain,
89+
client,
90+
}),
91+
).catch(() => undefined);
92+
if (!bytecode || bytecode === "0x") {
93+
throw new Error(
94+
`Error: ${fromTokenAddress} on chainId: ${chain.id} is not a valid contract address.`,
95+
);
96+
}
97+
}
98+
const params = {
99+
fromTokenAddress,
100+
to,
101+
chainId: String(chain.id),
102+
fromAmount: String(fromAmount),
103+
};
104+
const queryString = new URLSearchParams(params).toString();
105+
const url = `${getPayConvertCryptoToFiatEndpoint()}?${queryString}`;
106+
const response = await getClientFetch(client)(url);
107+
if (!response.ok) {
108+
throw new Error(
109+
`Failed to fetch ${to} value for token (${fromTokenAddress}) on chainId: ${chain.id}`,
110+
);
111+
}
112+
113+
const data: { result: number } = await response.json();
114+
return data;
115+
} catch (error) {
116+
console.error(error);
117+
throw new Error(
118+
`Failed to fetch ${to} value for token (${fromTokenAddress}) on chainId: ${chain.id}`,
119+
);
120+
}
121+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, expect, it } from "vitest";
2+
import { TEST_CLIENT } from "~test/test-clients.js";
3+
import { base } from "../../chains/chain-definitions/base.js";
4+
import { ethereum } from "../../chains/chain-definitions/ethereum.js";
5+
import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js";
6+
import { convertFiatToCrypto } from "./fiatToCrypto.js";
7+
8+
describe.runIf(process.env.TW_SECRET_KEY)("Pay: fiatToCrypto", () => {
9+
it("should convert fiat price to token on Ethereum mainnet", async () => {
10+
const data = await convertFiatToCrypto({
11+
chain: ethereum,
12+
from: "usd",
13+
fromAmount: 1,
14+
to: NATIVE_TOKEN_ADDRESS,
15+
client: TEST_CLIENT,
16+
});
17+
expect(data.result).toBeDefined();
18+
// Should be a number
19+
expect(!Number.isNaN(data.result)).toBe(true);
20+
// Since eth is around US$3000, 1 USD should be around 0.0003
21+
// we give it some safe margin so the test won't be flaky
22+
expect(Number(data.result) < 0.001).toBe(true);
23+
});
24+
25+
it("should convert fiat price to token on Base mainnet", async () => {
26+
const data = await convertFiatToCrypto({
27+
chain: base,
28+
from: "usd",
29+
fromAmount: 1,
30+
to: NATIVE_TOKEN_ADDRESS,
31+
client: TEST_CLIENT,
32+
});
33+
34+
expect(data.result).toBeDefined();
35+
// Should be a number
36+
expect(!Number.isNaN(data.result)).toBe(true);
37+
// Since eth is around US$3000, 1 USD should be around 0.0003
38+
// we give it some safe margin so the test won't be flaky
39+
expect(Number(data.result) < 0.001).toBe(true);
40+
});
41+
});

0 commit comments

Comments
 (0)