Documentation Index
Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt
Use this file to discover all available pages before exploring further.
Why Verify Events?
On StarkNet, a transaction can have status “SUCCEEDED” while the token transfer inside it fails silently. This happens because:
- The Chipi paymaster wraps your calls in
execute_from_outside_v2
- The forwarder contract executes the wrapped call
- If the inner call fails (e.g., insufficient balance), the forwarder may still succeed
- The transaction is marked “SUCCEEDED” on explorers
Always verify Transfer events to confirm tokens actually moved.
Checking Transfer Events via RPC
After a transfer, query the transaction receipt for Transfer events from the token contract:
const provider = new RpcProvider({
nodeUrl: "https://starknet-mainnet.infura.io/v3/YOUR_KEY",
});
const receipt = await provider.getTransactionReceipt(txHash);
if (receipt.execution_status !== "SUCCEEDED") {
throw new Error(`Transaction failed: ${receipt.execution_status}`);
}
// Transfer event key = starknet_keccak("Transfer")
const TRANSFER_KEY =
"0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9";
const USDC_ADDRESS =
"0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb";
const transferEvent = receipt.events.find((event) => {
const isUsdc = BigInt(event.from_address) === BigInt(USDC_ADDRESS);
const isTransfer = event.keys[0] === TRANSFER_KEY;
return isUsdc && isTransfer;
});
if (!transferEvent) {
throw new Error("No USDC Transfer event found — transfer failed silently");
}
// StarkNet Transfer events use indexed parameters (keys):
// keys[0] = Transfer selector
// keys[1] = from address
// keys[2] = to address
// data[0] = amount (low 128 bits)
// data[1] = amount (high 128 bits)
const from = transferEvent.keys[1];
const to = transferEvent.keys[2];
const amountLow = BigInt(transferEvent.data[0]);
const amountHigh = BigInt(transferEvent.data[1]);
const amount = amountLow + (amountHigh << 128n);
console.log(`Verified: ${amount} units from ${from} to ${to}`);
Checking Balance Before and After
For additional certainty, query the token balance before and after the transfer:
const balanceBefore = await getBalance(provider, tokenAddress, recipientAddress);
const txHash = await serverClient.transfer({ params: { ... } });
// Wait for the tx to be included in a block
await provider.waitForTransaction(txHash);
const balanceAfter = await getBalance(provider, tokenAddress, recipientAddress);
if (balanceAfter <= balanceBefore) {
throw new Error("Balance did not increase — transfer may have failed");
}
async function getBalance(provider, token, account) {
const result = await provider.callContract({
contractAddress: token,
entrypoint: "balance_of",
calldata: [account],
});
return BigInt(result[0]) + (BigInt(result[1]) << 128n);
}
Using getTransactionStatus for Polling
The SDK’s getTransactionStatus method polls the chain for finality, but it only checks transaction inclusion — not whether the inner transfer succeeded:
// This tells you the tx is on-chain, NOT that tokens moved
const status = await serverClient.getTransactionStatus({ hash: txHash });
// status.onChainStatus: "ACCEPTED_ON_L2" | "ACCEPTED_ON_L1" | "REJECTED" | ...
Use getTransactionStatus for finality, then verify Transfer events for correctness.
Common Pitfalls
| Pitfall | What Happens | How to Detect |
|---|
| Insufficient token balance | Tx succeeds, no Transfer event | Check events in receipt |
| Wrong recipient address | Tokens sent to wrong address | Verify keys[2] matches expected recipient |
| Amount overflow/underflow | Transfer of 0 tokens | Check data[0] and data[1] are non-zero |
| Tx status “SUCCEEDED” but transfer failed | Forwarder catches inner error | Always check Transfer events |
Related