-
Notifications
You must be signed in to change notification settings - Fork 586
[SDK] move over some utility read only endpoints to API #8005
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,3 +34,4 @@ storybook-static | |
tsconfig.tsbuildinfo | ||
.cursor | ||
apps/dashboard/node-compile-cache | ||
.tmp/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,10 @@ | |
} | ||
} | ||
}, | ||
"formatter": { | ||
"enabled": true, | ||
"indentStyle": "space" | ||
}, | ||
"includes": ["package.json"] | ||
} | ||
] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
import { | ||
getContractMetadata as apiGetContractMetadata, | ||
configure, | ||
} from "@thirdweb-dev/api"; | ||
Comment on lines
+1
to
+4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid global Same concern as in balance utils: - // Configure the API client
- configure({
- clientId: options.contract.client.clientId,
- secretKey: options.contract.client.secretKey,
- });
+ ensureApiConfigured(options.contract.client); Also applies to: 32-37 🤖 Prompt for AI Agents
|
||
import type { BaseTransactionOptions } from "../../../transaction/types.js"; | ||
import { fetchContractMetadata } from "../../../utils/contract/fetchContractMetadata.js"; | ||
import { contractURI } from "../__generated__/IContractMetadata/read/contractURI.js"; | ||
|
@@ -25,6 +29,72 @@ export async function getContractMetadata( | |
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix any | ||
[key: string]: any; | ||
}> { | ||
// Configure the API client | ||
configure({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of calling configure, pass |
||
clientId: options.contract.client.clientId, | ||
secretKey: options.contract.client.secretKey, | ||
}); | ||
|
||
try { | ||
// Try to get metadata from the API first | ||
const response = await apiGetContractMetadata({ | ||
path: { | ||
chainId: options.contract.chain.id, | ||
address: options.contract.address, | ||
}, | ||
}); | ||
|
||
if (response.data?.result?.output?.abi) { | ||
// Extract name and symbol from ABI or devdoc/userdoc | ||
const abi = response.data.result.output.abi; | ||
const devdoc = response.data.result.output.devdoc; | ||
const userdoc = response.data.result.output.userdoc; | ||
|
||
// Try to find name and symbol from ABI functions | ||
let contractName = ""; | ||
let contractSymbol = ""; | ||
Comment on lines
+47
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Early return can emit empty name/symbol; gate return or merge with fallback If - return {
- name: contractName,
- symbol: contractSymbol,
- abi: abi,
- compiler: response.data.result.compiler,
- language: response.data.result.language,
- devdoc: devdoc,
- userdoc: userdoc,
- };
+ if (contractName || contractSymbol) {
+ return {
+ name: contractName,
+ symbol: contractSymbol,
+ abi,
+ compiler: response.data.result.compiler,
+ language: response.data.result.language,
+ devdoc,
+ userdoc,
+ };
+ }
+ // No name/symbol resolvable via ABI/RPC; fall through to fallback below. Optional improvement: capture Also applies to: 71-79, 82-92 🤖 Prompt for AI Agents
|
||
|
||
if (Array.isArray(abi)) { | ||
const nameFunc = abi.find( | ||
(item: any) => | ||
item.type === "function" && | ||
item.name === "name" && | ||
item.inputs?.length === 0, | ||
); | ||
const symbolFunc = abi.find( | ||
(item: any) => | ||
item.type === "function" && | ||
item.name === "symbol" && | ||
item.inputs?.length === 0, | ||
); | ||
|
||
if (nameFunc || symbolFunc) { | ||
// Fall back to RPC if we found name/symbol functions in ABI | ||
const [resolvedName, resolvedSymbol] = await Promise.all([ | ||
nameFunc ? name(options).catch(() => null) : null, | ||
symbolFunc ? symbol(options).catch(() => null) : null, | ||
]); | ||
contractName = resolvedName || ""; | ||
contractSymbol = resolvedSymbol || ""; | ||
} | ||
} | ||
|
||
return { | ||
name: contractName, | ||
symbol: contractSymbol, | ||
abi: abi, | ||
compiler: response.data.result.compiler, | ||
language: response.data.result.language, | ||
devdoc: devdoc, | ||
userdoc: userdoc, | ||
}; | ||
} | ||
} catch (error) { | ||
// API failed, fall back to original implementation | ||
console.debug("Contract metadata API failed, falling back to RPC:", error); | ||
} | ||
|
||
// Fallback to original RPC-based implementation | ||
const [resolvedMetadata, resolvedName, resolvedSymbol] = await Promise.all([ | ||
contractURI(options) | ||
.then((uri) => { | ||
|
@@ -41,7 +111,6 @@ export async function getContractMetadata( | |
symbol(options).catch(() => null), | ||
]); | ||
|
||
// TODO: basic parsing? | ||
return { | ||
...resolvedMetadata, | ||
name: resolvedMetadata?.name ?? resolvedName, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,77 +1,45 @@ | ||
import { describe, expect, it } from "vitest"; | ||
import { ANVIL_CHAIN, FORKED_ETHEREUM_CHAIN } from "~test/chains.js"; | ||
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js"; | ||
import { TEST_CLIENT } from "~test/test-clients.js"; | ||
import { TEST_ACCOUNT_D } from "~test/test-wallets.js"; | ||
import { getContract } from "../../contract/contract.js"; | ||
import { mintTo } from "../../extensions/erc20/write/mintTo.js"; | ||
import { deployERC20Contract } from "../../extensions/prebuilts/deploy-erc20.js"; | ||
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js"; | ||
import { baseSepolia } from "../../chains/chain-definitions/base-sepolia.js"; | ||
import { getTokenBalance } from "./getTokenBalance.js"; | ||
|
||
const account = TEST_ACCOUNT_D; | ||
// Create a mock account for testing | ||
const testAccount = { | ||
address: "0x742d35Cc6645C0532b6C766684f4b4E99Bf87E8A", // Base deployer address | ||
}; | ||
|
||
describe.runIf(process.env.TW_SECRET_KEY)("getTokenBalance", () => { | ||
it("should work for native token", async () => { | ||
const result = await getTokenBalance({ | ||
account, | ||
chain: FORKED_ETHEREUM_CHAIN, | ||
account: testAccount, | ||
chain: baseSepolia, | ||
client: TEST_CLIENT, | ||
}); | ||
|
||
expect(result).toStrictEqual({ | ||
decimals: 18, | ||
displayValue: "10000", | ||
name: "Ether", | ||
symbol: "ETH", | ||
value: 10000000000000000000000n, | ||
}); | ||
expect(result).toBeDefined(); | ||
expect(result.decimals).toBe(18); | ||
expect(result.name).toBe("Sepolia Ether"); | ||
expect(result.symbol).toBe("ETH"); | ||
expect(result.value).toBeTypeOf("bigint"); | ||
expect(result.displayValue).toBeTypeOf("string"); | ||
}); | ||
|
||
it("should work for ERC20 token", async () => { | ||
const erc20Address = await deployERC20Contract({ | ||
account, | ||
chain: ANVIL_CHAIN, | ||
client: TEST_CLIENT, | ||
params: { | ||
contractURI: TEST_CONTRACT_URI, | ||
name: "", | ||
}, | ||
type: "TokenERC20", | ||
}); | ||
const erc20Contract = getContract({ | ||
address: erc20Address, | ||
chain: ANVIL_CHAIN, | ||
client: TEST_CLIENT, | ||
}); | ||
|
||
const amount = 1000; | ||
|
||
// Mint some tokens | ||
const tx = mintTo({ | ||
amount, | ||
contract: erc20Contract, | ||
to: account.address, | ||
}); | ||
|
||
await sendAndConfirmTransaction({ | ||
account, | ||
transaction: tx, | ||
}); | ||
// Use a known ERC20 token on Base Sepolia | ||
const usdcAddress = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; // USDC on Base Sepolia | ||
|
||
const result = await getTokenBalance({ | ||
account, | ||
chain: ANVIL_CHAIN, | ||
account: testAccount, | ||
chain: baseSepolia, | ||
client: TEST_CLIENT, | ||
tokenAddress: erc20Address, | ||
tokenAddress: usdcAddress, | ||
}); | ||
|
||
const expectedDecimal = 18; | ||
expect(result).toBeDefined(); | ||
expect(result.decimals).toBe(expectedDecimal); | ||
expect(result.decimals).toBe(6); // USDC has 6 decimals | ||
expect(result.symbol).toBeDefined(); | ||
expect(result.name).toBeDefined(); | ||
expect(result.value).toBe(BigInt(amount) * 10n ** BigInt(expectedDecimal)); | ||
expect(result.displayValue).toBe(amount.toString()); | ||
expect(result.value).toBeTypeOf("bigint"); | ||
expect(result.displayValue).toBeTypeOf("string"); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,9 @@ | ||
import type { Chain } from "../../chains/types.js"; | ||
import { | ||
getChainDecimals, | ||
getChainNativeCurrencyName, | ||
getChainSymbol, | ||
} from "../../chains/utils.js"; | ||
getWalletBalance as apiGetWalletBalance, | ||
configure, | ||
} from "@thirdweb-dev/api"; | ||
import type { Chain } from "../../chains/types.js"; | ||
Comment on lines
+2
to
+4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Global configure in a hot path can race across clients; avoid mutable global auth
Apply this change locally: - // Configure the API client with credentials from the thirdweb client
- configure({
- clientId: client.clientId,
- secretKey: client.secretKey,
- });
+ // Ensure API auth is set for this client (no-ops if already set for same creds)
+ ensureApiConfigured(client); Add a shared helper (place once, e.g., // internal/api/config.ts
import { configure } from "@thirdweb-dev/api";
import type { ThirdwebClient } from "../../client/client.js";
let activeClientId: string | undefined;
let activeSecret: string | undefined;
export function ensureApiConfigured(client: ThirdwebClient): void {
// Fast path: same creds already applied
if (client.clientId === activeClientId && client.secretKey === activeSecret) return;
// If different creds are already active, prefer explicitness:
// either throw or overwrite with a warning. Choose policy; for now, overwrite.
if (activeClientId && (client.clientId !== activeClientId || client.secretKey !== activeSecret)) {
// Consider routing these through a logger if available
// console.warn("Switching @thirdweb-dev/api credentials at runtime.");
}
configure({ clientId: client.clientId, secretKey: client.secretKey });
activeClientId = client.clientId;
activeSecret = client.secretKey;
} Also applies to: 42-47 |
||
import type { ThirdwebClient } from "../../client/client.js"; | ||
import { getContract } from "../../contract/contract.js"; | ||
import { eth_getBalance } from "../../rpc/actions/eth_getBalance.js"; | ||
import { getRpcClient } from "../../rpc/rpc.js"; | ||
import { toTokens } from "../../utils/units.js"; | ||
import type { Account } from "../interfaces/wallet.js"; | ||
|
||
type GetTokenBalanceOptions = { | ||
|
@@ -43,33 +38,40 @@ export async function getTokenBalance( | |
options: GetTokenBalanceOptions, | ||
): Promise<GetTokenBalanceResult> { | ||
const { account, client, chain, tokenAddress } = options; | ||
// erc20 case | ||
if (tokenAddress) { | ||
// load balanceOf dynamically to avoid circular dependency | ||
const { getBalance } = await import( | ||
"../../extensions/erc20/read/getBalance.js" | ||
); | ||
return getBalance({ | ||
|
||
// Configure the API client with credentials from the thirdweb client | ||
configure({ | ||
clientId: client.clientId, | ||
secretKey: client.secretKey, | ||
}); | ||
|
||
const response = await apiGetWalletBalance({ | ||
path: { | ||
address: account.address, | ||
contract: getContract({ address: tokenAddress, chain, client }), | ||
}); | ||
}, | ||
query: { | ||
chainId: [chain.id], | ||
...(tokenAddress && { tokenAddress }), | ||
}, | ||
}); | ||
|
||
if (!response.data?.result || response.data.result.length === 0) { | ||
throw new Error("No balance data returned from API"); | ||
} | ||
// native token case | ||
const rpcRequest = getRpcClient({ chain, client }); | ||
|
||
const [nativeSymbol, nativeDecimals, nativeName, nativeBalance] = | ||
await Promise.all([ | ||
getChainSymbol(chain), | ||
getChainDecimals(chain), | ||
getChainNativeCurrencyName(chain), | ||
eth_getBalance(rpcRequest, { address: account.address }), | ||
]); | ||
// Get the first result (should match our chain) | ||
const balanceData = response.data.result[0]; | ||
|
||
if (!balanceData) { | ||
throw new Error("Balance data not found for the specified chain"); | ||
} | ||
|
||
// Transform API response to match the existing GetTokenBalanceResult interface | ||
return { | ||
decimals: nativeDecimals, | ||
displayValue: toTokens(nativeBalance, nativeDecimals), | ||
name: nativeName, | ||
symbol: nativeSymbol, | ||
value: nativeBalance, | ||
decimals: balanceData.decimals, | ||
displayValue: balanceData.displayValue, | ||
name: balanceData.name, | ||
symbol: balanceData.symbol, | ||
value: BigInt(balanceData.value), | ||
}; | ||
} |
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.
i think that extends: "//" is wrong no? that's prob why its not inheriting the root rules?