mcpField
S
starknet-js

starknet-js

github.com/keep-starknet-strange开发8141 次安装3 周前更新
代码仓库

关于 starknet-js

面向使用 TypeScript 构建 Starknet 应用的开发者,starknet.js v9.x SDK 提供网络读取、账户交易和合约交互接口。还涵盖多调用、交易模拟、钱包连接及 Paymaster 操作。

主要功能

  1. 01连接 Starknet RPC
  2. 02管理账户与交易
  3. 03读取和调用合约
  4. 04批量执行多调用
  5. 05模拟交易并估算费用
  6. 06支持钱包与 Paymaster

适用场景

  1. 01读取 Starknet 网络和合约状态
  2. 02提交合约交易并等待回执
  3. 03批量执行合约调用
  4. 04为应用集成钱包或代付 gas

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/keep-starknet-strange/starknet-agentic --skill starknet-js

安装后在 Claude Code 中输入/starknet-js确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

package.json
tsconfig.json
agents/
openai.yaml
references/
account-lifecycle.md
fee-strategy.md
provider-hardening.md
README.md
scripts/
account-example.ts

详细介绍

starknet.js v9.x SDK

相关模块:技能目录。

适用场景

  • 使用 provider、账户、合约、钱包或 paymaster 流程构建 Starknet 应用。

不适用场景

  • Cairo 合约编写、仅用于部署的操作手册或安全审计。

快速开始

npm install starknet

读取 Starknet 的最简配置:

import { RpcProvider, Contract } from 'starknet';

const provider = await RpcProvider.create({ nodeUrl: 'https://rpc.starknet.lava.build' });
const contract = new Contract(abi, contractAddress, provider);
const result = await contract.get_balance();

核心架构

Provider -> Account -> Contract
   |          |          |
Network   Identity   Interaction
  • Provider:只读网络连接(RpcProvider)
  • Account:在 Provider 的基础上扩展签名和交易功能
  • Contract:已部署合约的类型安全接口

读取操作使用 Provider,写入操作使用 Account。

Provider 配置

import { RpcProvider } from 'starknet';

// Recommended: Auto-detect RPC spec version
const provider = await RpcProvider.create({
  nodeUrl: 'https://rpc.starknet.lava.build'
});

网络:

  • 主网:https://rpc.starknet.lava.build
  • Sepolia:https://rpc.starknet-testnet.lava.build

主要方法:

const chainId = await provider.getChainId();
const block = await provider.getBlock('latest');
const nonce = await provider.getNonceForAddress(accountAddress);
await provider.waitForTransaction(txHash);

// Read storage directly
const value = await provider.getStorageAt(contractAddress, storageKey);

账户管理

创建账户(4 个步骤)

步骤 1:计算地址

import { hash, ec, encode, CallData } from 'starknet';

// IMPORTANT: `stark.randomAddress()` returns an address-like random felt and is NOT a private key.
// Use a real stark curve private key generator.
const privateKey = '0x' + encode.buf2hex(ec.starkCurve.utils.randomPrivateKey());
const publicKey = ec.starkCurve.getStarkKey(privateKey);

// NOTE: account class hashes are network/account-type dependent.
// Treat this as an example only (verify the correct class hash for your setup).
const classHash = '0x540d7f5ec7ecf317e68d48564934cb99259781b1ee3cedbbc37ec5337f8e688'; // example

const constructorCalldata = CallData.compile({ publicKey });
const address = hash.calculateContractAddressFromHash(publicKey, classHash, constructorCalldata, 0);

步骤 2:为地址注资,部署前存入 STRK。

步骤 3:部署

import { Account } from 'starknet';

// NOTE: Account constructor signature varies across starknet.js versions.
// If this doesn't typecheck for your version, refer to the official docs.
const account = new Account({ provider, address, signer: privateKey, cairoVersion: '1' });
const { transaction_hash } = await account.deployAccount({
  classHash,
  constructorCalldata,
  addressSalt: publicKey
});
await provider.waitForTransaction(transaction_hash);

步骤 4:使用账户执行交易。

连接到现有账户
const account = new Account({
  provider,
  address: '0x123...',
  signer: privateKey,
  cairoVersion: '1'  // Optional, auto-detected if omitted
});

合约交互

连接到合约
import { Contract } from 'starknet';

const contract = new Contract(abi, contractAddress, provider);  // Read-only
const writeContract = new Contract(abi, contractAddress, account);   // Read-write
类型化合约(类型安全)
// Get full TypeScript autocomplete and type checking from ABI
const typedContract = contract.typedv2(abi);
const balance = await typedContract.balanceOf(userAddress);
读取状态
const balance = await contract.get_balance();
const userBalance = await contract.balanceOf(userAddress);
写入(执行)
const tx = await contract.increase_balance(100);
await provider.waitForTransaction(tx.transaction_hash);
Multicall(批量交易)
import { CallData, cairo } from 'starknet';

const calls = [
  {
    contractAddress: tokenAddress,
    entrypoint: 'approve',
    calldata: CallData.compile({ spender: bridgeAddress, amount: cairo.uint256(1000n) })
  },
  {
    contractAddress: bridgeAddress,
    entrypoint: 'deposit',
    calldata: CallData.compile({ amount: cairo.uint256(1000n) })
  }
];

const tx = await account.execute(calls);

使用 populate() 确保类型安全:

const approveCall = tokenContract.populate('approve', {
  spender: bridgeAddress,
  amount: cairo.uint256(1000n)
});
const depositCall = bridgeContract.populate('deposit', { amount: cairo.uint256(1000n) });
const tx = await account.execute([approveCall, depositCall]);
解析事件
const receipt = await provider.getTransactionReceipt(txHash);
const events = contract.parseEvents(receipt);
const transferEvents = contract.parseEvents(receipt, 'Transfer');

交易模拟

执行前先进行模拟,以捕获回滚并检查状态变更:

const simResult = await account.simulateTransaction(
  [{ type: 'INVOKE', payload: calls }],
  { skipValidate: false }
);

console.log('Fee estimate:', simResult[0].fee_estimation);
console.log('Trace:', simResult[0].transaction_trace);

// Check state changes before execution
const trace = simResult[0].transaction_trace;
if (trace?.state_diff) {
  console.log('Storage changes:', trace.state_diff.storage_diffs);
}

费用估算

const fee = await account.estimateInvokeFee(calls);
console.log({
  overallFee: fee.overall_fee,
  resourceBounds: fee.resourceBounds  // V3: l1_gas, l2_gas, l1_data_gas
});

使用自定义限额执行:

const tx = await account.execute(calls, {
  resourceBounds: {
    l1_gas: { amount: '0x2000', price: '0x1000000000' },
    l2_gas: { amount: '0x0', price: '0x0' },
    l1_data_gas: { amount: '0x1000', price: '0x1000000000' }
  }
});

附带优先费:

const tipStats = await provider.getEstimateTip();
const tx = await account.execute(calls, { tip: tipStats.percentile_75 });

交易收据处理

const receipt = await provider.waitForTransaction(txHash);

// Status check helpers
if (receipt.isSuccess()) {
  console.log('Transaction succeeded');
} else if (receipt.isReverted()) {
  console.log('Reverted:', receipt.revert_reason);
} else if (receipt.isRejected()) {
  console.log('Rejected');
} else if (receipt.isError()) {
  console.log('Error');
}

钱包集成

连接浏览器钱包(ArgentX、Braavos):

import { connect } from '@starknet-io/get-starknet';
import { WalletAccount } from 'starknet';

const selectedWallet = await connect({ modalMode: 'alwaysAsk' });
const walletAccount = await WalletAccount.connect(
  { nodeUrl: 'https://rpc.starknet.lava.build' },
  selectedWallet
);

// Use like regular Account
const tx = await walletAccount.execute(calls);

// Event handlers
walletAccount.onAccountChange((accounts) => console.log('New account:', accounts[0]));
walletAccount.onNetworkChanged((chainId) => console.log('Network changed:', chainId));

Paymaster(Gas 赞助)

配置 paymaster,以支持赞助交易或使用替代 Gas 代币的交易:

import { PaymasterRpc, Account } from 'starknet';

const paymaster = new PaymasterRpc({ nodeUrl: 'https://sepolia.paymaster.avnu.fi' });
const account = new Account({ provider, address, signer: privateKey, paymaster });

赞助交易(dApp 支付 Gas):

const tx = await account.executePaymasterTransaction(calls, { feeMode: { mode: 'sponsored' } });

替代代币(例如 USDC):

const tokens = await account.paymaster.getSupportedTokens();
const feeDetails = { feeMode: { mode: 'default', gasToken: USDC_ADDRESS } };
const estimate = await account.estimatePaymasterTransactionFee(calls, feeDetails);
const tx = await account.executePaymasterTransaction(calls, feeDetails, estimate.suggested_max_fee_in_gas_token);

消息签名(SNIP-12)

const typedData = {
  types: {
    StarknetDomain: [
      { name: 'name', type: 'shortstring' },
      { name: 'version', type: 'shortstring' },
      { name: 'chainId', type: 'shortstring' },
      { name: 'revision', type: 'shortstring' }
    ],
    Message: [{ name: 'content', type: 'shortstring' }]
  },
  primaryType: 'Message',
  domain: { name: 'MyDapp', version: '1', chainId: 'SN_SEPOLIA', revision: '1' },
  message: { content: 'Hello Starknet' }
};

const signature = await account.signMessage(typedData);
const msgHash = await account.hashMessage(typedData);
const isValid = ec.starkCurve.verify(signature, msgHash, publicKey);

CallData 与 Cairo 类型

import { CallData, cairo, CairoCustomEnum, CairoOption, CairoOptionVariant } from 'starknet';

// Compile with ABI
const calldata = new CallData(abi);
const compiled = calldata.compile('transfer', { recipient: '0x...', amount: cairo.uint256(1000n) });

// Cairo type helpers - always use BigInt (n suffix) for token amounts
cairo.uint256(1000n)          // { low, high } - ALWAYS use BigInt for precision
cairo.felt252(1000)           // BigInt
cairo.felt('0x123')           // hex to felt
cairo.bool(true)              // Cairo bool
cairo.byteArray('Hello')      // ByteArray for long strings

// Short strings (<= 31 chars)
import { shortString } from 'starknet';
shortString.encodeShortString('hello')  // felt252
shortString.decodeShortString('0x...')  // 'hello'

// Enums and Options
const myEnum = new CairoCustomEnum({ Variant1: { value: 123 } });
const some = new CairoOption(CairoOptionVariant.Some, value);

**重要:**代币金额和余额始终使用 BigInt(例如 1000n)。切勿对 wei 值使用 Number() 或 parseFloat()——JavaScript 数字在超过 2^53 时会失去精度。

ERC-20 代币操作

const erc20 = new Contract(erc20Abi, tokenAddress, account);

// Read balance (returns BigInt - do NOT convert with Number())
const balance = await erc20.balanceOf(account.address);
console.log('Balance (wei):', balance.toString());

// Transfer (use BigInt for amount)
const amount = cairo.uint256(1000000000000000000n); // 1 token (18 decimals)
const tx = await erc20.transfer(recipientAddress, amount);
await provider.waitForTransaction(tx.transaction_hash);

// Approve + transferFrom pattern
await erc20.approve(spenderAddress, cairo.uint256(amount));

实用函数

import { stark, ec, encode, num, hash } from 'starknet';

// Key generation
const privateKey = '0x' + encode.buf2hex(ec.starkCurve.utils.randomPrivateKey());
const publicKey = ec.starkCurve.getStarkKey(privateKey);

// Number conversions
num.toHex(123);           // '0x7b'
num.toBigInt('0x7b');     // 123n

// Hashing
hash.getSelectorFromName('transfer');
hash.calculateContractAddressFromHash(salt, classHash, calldata, deployer);

合约部署

// Deploy via UDC
const { transaction_hash, contract_address } = await account.deploy({
  classHash: '0x...',
  constructorCalldata: CallData.compile({ owner: account.address }),
  salt: stark.randomAddress(), // random felt252 salt (not a private key)
  unique: true
});

// Declare first, then deploy
const declareResponse = await account.declare({
  contract: compiledSierra,
  casm: compiledCasm
});
await provider.waitForTransaction(declareResponse.transaction_hash);

const deployResponse = await account.deploy({
  classHash: declareResponse.class_hash,
  constructorCalldata: CallData.compile({ owner: account.address })
});

// Or combined
const result = await account.declareAndDeploy({
  contract: compiledContract,
  casm: compiledCasm,
  constructorCalldata: CallData.compile({ owner: account.address })
});

外部执行(SNIP-9)

代表其他账户执行交易(免 Gas/委托执行):

const version = await account.getSnip9Version();  // 'V1' | 'V2' | 'UNSUPPORTED'

const outsideTransaction = await account.getOutsideTransaction(
  { caller: executorAddress, execute_after: now, execute_before: now + 3600 },
  calls,
  'V2'
);

// Executor submits the pre-signed transaction
const result = await executorAccount.executeFromOutside(outsideTransaction);

错误处理

import { LibraryError, RpcError } from 'starknet';

try {
  const tx = await account.execute(calls);
} catch (error) {
  if (error instanceof RpcError) {
    console.error('RPC error:', error.code, error.message);
  } else if (error instanceof LibraryError) {
    console.error('Library error:', error.message);
  }
}

日志与配置

import { config, setLogLevel } from 'starknet';

// Global config
config.set('transactionVersion', '0x3');
config.get('transactionVersion');

// Logging
setLogLevel('DEBUG');  // ERROR | WARN | INFO | DEBUG

评论

支持 Markdown