starknet-js について
starknet.js v9.x は、TypeScript で Starknet アプリを開発するための SDK です。ネットワーク接続、アカウント、コントラクト操作に加え、マルチコール、トランザクションのシミュレーション、ウォレット接続、Paymaster 操作を扱います。
主な機能
- 01Starknet RPC への接続
- 02アカウントとトランザクションの管理
- 03コントラクトの読み取りと呼び出し
- 04複数の呼び出しを一括実行
- 05シミュレーションと手数料見積もり
- 06ウォレットと Paymaster の統合
活用シーン
- 01Starknet のネットワークやコントラクト状態の読み取り
- 02コントラクトトランザクションの送信とレシート待機
- 03コントラクト呼び出しの一括処理
- 04アプリへのウォレットやガス代スポンサーの追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ keep-starknet-strange/ starknet-agentic --skill starknet-jsインストール後、Claude Code で/starknet-jsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
package.jsontsconfig.jsonagents/openai.yamlreferences/account-lifecycle.mdfee-strategy.mdprovider-hardening.mdREADME.mdscripts/account-example.ts詳細
starknet.js v9.x SDK
関連モジュール:スキルカタログ。
使用する場面
- プロバイダー、アカウント、コントラクト、ウォレット、またはペイマスターのフローを使用して、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
- プロバイダー:読み取り専用のネットワーク接続(RpcProvider)
- アカウント:プロバイダーを拡張した、署名およびトランザクション機能
- コントラクト:デプロイ済みコントラクトへの型安全なインターフェース
読み取り操作にはプロバイダーを、書き込み操作にはアカウントを使用します。
プロバイダーのセットアップ
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);
マルチコール(トランザクションの一括処理)
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));
ペイマスター(ガスのスポンサー提供)
スポンサー付き、または代替ガストークンを使用したトランザクション用にペイマスターをセットアップします:
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 がガスを支払う):
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 })
});
Outside Execution(SNIP-9)
別のアカウントに代わってトランザクションを実行します(ガスレス/委任):
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