-
Notifications
You must be signed in to change notification settings - Fork 471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SDK] Add 2 new Pay functions: convertFiatToCrypto and convertCryptoToFiat #5457
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
--- | ||
"thirdweb": minor | ||
--- | ||
|
||
Add 2 new Pay functions: convertFiatToCrypto and convertCryptoToFiat | ||
|
||
Examples: | ||
|
||
### Convert fiat (USD) to crypto | ||
```ts | ||
import { convertFiatToCrypto } from "thirdweb/pay"; | ||
import { ethereum } from "thirdweb/chains"; | ||
|
||
// Convert 2 cents to ETH | ||
const result = await convertFiatToCrypto({ | ||
from: "USD", | ||
// the token address. For native token, use NATIVE_TOKEN_ADDRESS | ||
to: "0x...", | ||
// the chain (of the chain where the token belong to) | ||
chain: ethereum, | ||
// 2 cents | ||
fromAmount: 0.02, | ||
}); | ||
// Result: 0.0000057 (a number) | ||
``` | ||
|
||
### Convert crypto to fiat (USD) | ||
|
||
```ts | ||
import { convertCryptoToFiat } from "thirdweb/pay"; | ||
|
||
// Get Ethereum price | ||
const result = convertCryptoToFiat({ | ||
fromTokenAddress: NATIVE_TOKEN_ADDRESS, | ||
to: "USD", | ||
chain: ethereum, | ||
fromAmount: 1, | ||
}); | ||
|
||
// Result: 3404.11 (number) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { describe, expect, it } from "vitest"; | ||
import { TEST_CLIENT } from "~test/test-clients.js"; | ||
import { TEST_ACCOUNT_A } from "~test/test-wallets.js"; | ||
import { base } from "../../chains/chain-definitions/base.js"; | ||
import { ethereum } from "../../chains/chain-definitions/ethereum.js"; | ||
import { sepolia } from "../../chains/chain-definitions/sepolia.js"; | ||
import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js"; | ||
import { convertCryptoToFiat } from "./cryptoToFiat.js"; | ||
|
||
describe.runIf(process.env.TW_SECRET_KEY)("Pay: crypto-to-fiat", () => { | ||
it("should convert ETH price to USD on Ethereum mainnet", async () => { | ||
const data = await convertCryptoToFiat({ | ||
chain: ethereum, | ||
fromTokenAddress: NATIVE_TOKEN_ADDRESS, | ||
fromAmount: 1, | ||
to: "USD", | ||
client: TEST_CLIENT, | ||
}); | ||
expect(data.result).toBeDefined(); | ||
// Should be a number | ||
expect(!Number.isNaN(data.result)).toBe(true); | ||
// Since eth is around US$3000, we can add a test to check if the price is greater than $1500 (as a safe margin) | ||
// let's hope that scenario does not happen :( | ||
expect(Number(data.result) > 1500).toBe(true); | ||
}); | ||
|
||
it("should convert ETH price to USD on Base mainnet", async () => { | ||
const data = await convertCryptoToFiat({ | ||
chain: base, | ||
fromTokenAddress: NATIVE_TOKEN_ADDRESS, | ||
fromAmount: 1, | ||
to: "USD", | ||
client: TEST_CLIENT, | ||
}); | ||
expect(data.result).toBeDefined(); | ||
// Should be a number | ||
expect(!Number.isNaN(data.result)).toBe(true); | ||
// Since eth is around US$3000, we can add a test to check if the price is greater than $1500 (as a safe margin) | ||
// let's hope that scenario does not happen :( | ||
expect(data.result > 1500).toBe(true); | ||
}); | ||
|
||
it("should return zero if fromAmount is zero", async () => { | ||
const data = await convertCryptoToFiat({ | ||
chain: base, | ||
fromTokenAddress: NATIVE_TOKEN_ADDRESS, | ||
fromAmount: 0, | ||
to: "USD", | ||
client: TEST_CLIENT, | ||
}); | ||
expect(data.result).toBe(0); | ||
}); | ||
|
||
it("should throw error for testnet chain (because testnets are not supported", async () => { | ||
kien-ngo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
await expect(() => | ||
convertCryptoToFiat({ | ||
chain: sepolia, | ||
fromTokenAddress: NATIVE_TOKEN_ADDRESS, | ||
fromAmount: 1, | ||
to: "USD", | ||
client: TEST_CLIENT, | ||
}), | ||
).rejects.toThrowError( | ||
`Cannot fetch price for a testnet (chainId: ${sepolia.id})`, | ||
); | ||
}); | ||
|
||
it("should throw error if fromTokenAddress is set to an invalid EVM address", async () => { | ||
await expect(() => | ||
convertCryptoToFiat({ | ||
chain: ethereum, | ||
fromTokenAddress: "haha", | ||
fromAmount: 1, | ||
to: "USD", | ||
client: TEST_CLIENT, | ||
}), | ||
).rejects.toThrowError( | ||
"Invalid fromTokenAddress. Expected a valid EVM contract address", | ||
); | ||
}); | ||
|
||
it("should throw error if fromTokenAddress is set to a wallet address", async () => { | ||
await expect(() => | ||
convertCryptoToFiat({ | ||
chain: base, | ||
fromTokenAddress: TEST_ACCOUNT_A.address, | ||
fromAmount: 1, | ||
to: "USD", | ||
client: TEST_CLIENT, | ||
}), | ||
).rejects.toThrowError( | ||
`Error: ${TEST_ACCOUNT_A.address} on chainId: ${base.id} is not a valid contract address.`, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import type { Address } from "abitype"; | ||
import type { Chain } from "../../chains/types.js"; | ||
import type { ThirdwebClient } from "../../client/client.js"; | ||
import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js"; | ||
import { getBytecode } from "../../contract/actions/get-bytecode.js"; | ||
import { getContract } from "../../contract/contract.js"; | ||
import { isAddress } from "../../utils/address.js"; | ||
import { getClientFetch } from "../../utils/fetch.js"; | ||
import { getPayConvertCryptoToFiatEndpoint } from "../utils/definitions.js"; | ||
|
||
/** | ||
* Props for the `convertCryptoToFiat` function | ||
* @buyCrypto | ||
*/ | ||
export type ConvertCryptoToFiatParams = { | ||
client: ThirdwebClient; | ||
/** | ||
* The contract address of the token | ||
* For native token, use NATIVE_TOKEN_ADDRESS | ||
*/ | ||
fromTokenAddress: Address; | ||
/** | ||
* The amount of token to convert to fiat value | ||
*/ | ||
fromAmount: number; | ||
/** | ||
* The chain that the token is deployed to | ||
*/ | ||
chain: Chain; | ||
/** | ||
* The fiat symbol. e.g "USD" | ||
* Only USD is supported at the moment. | ||
*/ | ||
to: "USD"; | ||
}; | ||
|
||
/** | ||
* Get a price of a token (using tokenAddress + chainId) in fiat. | ||
* Only USD is supported at the moment. | ||
* @example | ||
* ### Basic usage | ||
* For native token (non-ERC20), you should use NATIVE_TOKEN_ADDRESS as the value for `tokenAddress` | ||
* ```ts | ||
* import { convertCryptoToFiat } from "thirdweb/pay"; | ||
* | ||
* // Get Ethereum price | ||
* const result = convertCryptoToFiat({ | ||
* fromTokenAddress: NATIVE_TOKEN_ADDRESS, | ||
* to: "USD", | ||
* chain: ethereum, | ||
* fromAmount: 1, | ||
* }); | ||
* | ||
* // Result: 3404.11 | ||
* ``` | ||
* @buyCrypto | ||
* @returns a number representing the price (in selected fiat) of "x" token, with "x" being the `fromAmount`. | ||
*/ | ||
export async function convertCryptoToFiat( | ||
options: ConvertCryptoToFiatParams, | ||
): Promise<{ result: number }> { | ||
const { client, fromTokenAddress, to, chain, fromAmount } = options; | ||
if (Number(fromAmount) === 0) { | ||
return { result: 0 }; | ||
} | ||
// Testnets just don't work with our current provider(s) | ||
if (chain.testnet === true) { | ||
throw new Error(`Cannot fetch price for a testnet (chainId: ${chain.id})`); | ||
} | ||
// Some provider that we are using will return `0` for unsupported token | ||
// so we should do some basic input validations before sending the request | ||
|
||
// Make sure it's a valid EVM address | ||
if (!isAddress(fromTokenAddress)) { | ||
throw new Error( | ||
"Invalid fromTokenAddress. Expected a valid EVM contract address", | ||
); | ||
} | ||
// Make sure it's either a valid contract or a native token address | ||
if (fromTokenAddress.toLowerCase() !== NATIVE_TOKEN_ADDRESS.toLowerCase()) { | ||
const bytecode = await getBytecode( | ||
getContract({ | ||
address: fromTokenAddress, | ||
chain, | ||
client, | ||
}), | ||
).catch(() => undefined); | ||
if (!bytecode || bytecode === "0x") { | ||
throw new Error( | ||
graphite-app[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`Error: ${fromTokenAddress} on chainId: ${chain.id} is not a valid contract address.`, | ||
graphite-app[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
); | ||
} | ||
} | ||
const params = { | ||
fromTokenAddress, | ||
to, | ||
chainId: String(chain.id), | ||
fromAmount: String(fromAmount), | ||
}; | ||
const queryString = new URLSearchParams(params).toString(); | ||
const url = `${getPayConvertCryptoToFiatEndpoint()}?${queryString}`; | ||
const response = await getClientFetch(client)(url); | ||
if (!response.ok) { | ||
throw new Error( | ||
`Failed to fetch ${to} value for token (${fromTokenAddress}) on chainId: ${chain.id}`, | ||
); | ||
} | ||
|
||
const data: { result: number } = await response.json(); | ||
return data; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { describe, expect, it } from "vitest"; | ||
import { TEST_CLIENT } from "~test/test-clients.js"; | ||
import { TEST_ACCOUNT_A } from "~test/test-wallets.js"; | ||
import { base } from "../../chains/chain-definitions/base.js"; | ||
import { ethereum } from "../../chains/chain-definitions/ethereum.js"; | ||
import { sepolia } from "../../chains/chain-definitions/sepolia.js"; | ||
import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js"; | ||
import { convertFiatToCrypto } from "./fiatToCrypto.js"; | ||
|
||
describe.runIf(process.env.TW_SECRET_KEY)("Pay: fiatToCrypto", () => { | ||
it("should convert fiat price to token on Ethereum mainnet", async () => { | ||
const data = await convertFiatToCrypto({ | ||
chain: ethereum, | ||
from: "USD", | ||
fromAmount: 1, | ||
to: NATIVE_TOKEN_ADDRESS, | ||
client: TEST_CLIENT, | ||
}); | ||
expect(data.result).toBeDefined(); | ||
// Should be a number | ||
expect(!Number.isNaN(data.result)).toBe(true); | ||
// Since eth is around US$3000, 1 USD should be around 0.0003 | ||
// we give it some safe margin so the test won't be flaky | ||
expect(data.result < 0.001).toBe(true); | ||
}); | ||
|
||
it("should convert fiat price to token on Base mainnet", async () => { | ||
const data = await convertFiatToCrypto({ | ||
chain: base, | ||
from: "USD", | ||
fromAmount: 1, | ||
to: NATIVE_TOKEN_ADDRESS, | ||
client: TEST_CLIENT, | ||
}); | ||
|
||
expect(data.result).toBeDefined(); | ||
// Should be a number | ||
expect(!Number.isNaN(data.result)).toBe(true); | ||
// Since eth is around US$3000, 1 USD should be around 0.0003 | ||
// we give it some safe margin so the test won't be flaky | ||
expect(data.result < 0.001).toBe(true); | ||
}); | ||
|
||
it("should return zero if the fromAmount is zero", async () => { | ||
const data = await convertFiatToCrypto({ | ||
chain: base, | ||
from: "USD", | ||
fromAmount: 0, | ||
to: NATIVE_TOKEN_ADDRESS, | ||
client: TEST_CLIENT, | ||
}); | ||
expect(data.result).toBe(0); | ||
}); | ||
|
||
it("should throw error for testnet chain (because testnets are not supported", async () => { | ||
await expect(() => | ||
convertFiatToCrypto({ | ||
chain: sepolia, | ||
to: NATIVE_TOKEN_ADDRESS, | ||
fromAmount: 1, | ||
from: "USD", | ||
client: TEST_CLIENT, | ||
}), | ||
).rejects.toThrowError( | ||
`Cannot fetch price for a testnet (chainId: ${sepolia.id})`, | ||
); | ||
}); | ||
|
||
it("should throw error if `to` is set to an invalid EVM address", async () => { | ||
await expect(() => | ||
convertFiatToCrypto({ | ||
chain: ethereum, | ||
to: "haha", | ||
fromAmount: 1, | ||
from: "USD", | ||
client: TEST_CLIENT, | ||
}), | ||
).rejects.toThrowError( | ||
"Invalid `to`. Expected a valid EVM contract address", | ||
); | ||
}); | ||
|
||
it("should throw error if `to` is set to a wallet address", async () => { | ||
await expect(() => | ||
convertFiatToCrypto({ | ||
chain: base, | ||
to: TEST_ACCOUNT_A.address, | ||
fromAmount: 1, | ||
from: "USD", | ||
client: TEST_CLIENT, | ||
}), | ||
).rejects.toThrowError( | ||
`Error: ${TEST_ACCOUNT_A.address} on chainId: ${base.id} is not a valid contract address.`, | ||
); | ||
}); | ||
}); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add a quick example in the changeset?