opnet
Version:
The perfect library for building Bitcoin-based applications.
366 lines (279 loc) • 8.64 kB
Markdown
# Contract Interactions Overview
Smart contracts on OPNet are interacted with through the `Contract` class and `getContract()` factory function. This guide provides an overview of how contract interactions work.
## Overview
```mermaid
flowchart TB
subgraph Setup["Contract Setup"]
ABI[Import ABI]
Get[getContract]
Instance[Contract Instance]
end
subgraph Interaction["Contract Interaction"]
Sim[Simulate Call]
Result[CallResult]
Send[sendTransaction]
end
subgraph Network["Network"]
Provider[Provider]
Node[OPNet Node]
end
ABI --> Get
Get --> Instance
Instance --> Sim
Sim --> Provider
Provider --> Node
Node --> Result
Result --> Send
Send --> Provider
```
## Contract Interaction Flow
Contract interactions follow a consistent pattern:
```mermaid
sequenceDiagram
participant App as Your App
participant Contract as Contract
participant Provider as Provider
participant Node as OPNet Node
participant VM as Contract VM
App->>Contract: method(args)
Contract->>Contract: Encode calldata
Contract->>Provider: call(address, calldata)
Provider->>Node: JSON-RPC request
Node->>VM: Execute contract
VM-->>Node: Result/Revert
Node-->>Provider: Response
Provider-->>Contract: Decode result
Contract-->>App: CallResult<T>
```
## Simulation vs Execution
OPNet separates contract calls into two phases:
### Simulation (Read Operations)
Simulations execute contract code without creating a transaction:
```typescript
// Simulation - no Bitcoin spent
const balance = await contract.balanceOf(address);
console.log('Balance:', balance.properties.balance);
```
**Use simulations for:**
- Reading contract state
- Checking balances
- Verifying transactions before sending
- Querying metadata (name, symbol, etc.)
### Execution (Write Operations)
Execution creates a Bitcoin transaction that triggers contract execution:
```typescript
// First simulate
const simulation = await contract.transfer(recipient, amount, new Uint8Array(0));
// Then execute (requires Bitcoin)
const tx = await simulation.sendTransaction({
signer: wallet.keypair,
mldsaSigner: wallet.mldsaKeypair,
refundTo: wallet.p2tr,
network: network,
// ... other params
});
```
**Use execution for:**
- Transferring tokens
- Modifying contract state
- Deploying contracts
- Any state-changing operation
## ABI System
ABIs (Application Binary Interface) define the contract interface:
```typescript
import { OP_20_ABI, IOP20Contract } from 'opnet';
// ABI provides:
// - Function signatures
// - Parameter types
// - Return types
// - Event definitions
```
### Built-in ABIs
| ABI | Interface | Description |
|-----|-----------|-------------|
| `OP_20_ABI` | `IOP20Contract` | Fungible tokens |
| `OP_721_ABI` | `IOP721Contract` | Non-fungible tokens |
| `MOTOSWAP_ROUTER_ABI` | `IMotoswapRouterContract` | DEX router |
| `MotoswapPoolAbi` | `IMotoswapPoolContract` | Liquidity pool |
| `MotoSwapFactoryAbi` | `IMotoswapFactoryContract` | Pair factory |
### Custom ABIs
You can define custom ABIs for your contracts:
```typescript
const myAbi = {
functions: [
{
name: 'myMethod',
inputs: [{ name: 'value', type: 'UINT256' }],
outputs: [{ name: 'result', type: 'BOOL' }],
},
],
events: [
{
name: 'MyEvent',
values: [{ name: 'data', type: 'UINT256' }],
},
],
};
```
## Contract Types
The library provides a type hierarchy for contracts:
```mermaid
classDiagram
class IContract {
<<interface>>
+address: string
+provider: AbstractRpcProvider
}
class BaseContract {
+address: string
+network: Network
+interface: BitcoinInterface
+provider: AbstractRpcProvider
+from?: Address
+p2op: string
+setSender(sender)
+encodeCalldata(name, args)
+decodeEvents(events)
}
class IOP20Contract {
<<interface>>
+name()
+symbol()
+decimals()
+totalSupply()
+balanceOf(address)
+transfer(to, amount)
+approve(spender, amount)
+transferFrom(from, to, amount)
}
class IOP721Contract {
<<interface>>
+name()
+symbol()
+ownerOf(tokenId)
+balanceOf(owner)
+tokenURI(tokenId)
}
IContract <|-- BaseContract
BaseContract <|-- IOP20Contract
BaseContract <|-- IOP721Contract
```
## CallResult Class
Every contract call returns a `CallResult` object:
```typescript
const result = await contract.balanceOf(address);
// Access decoded properties
result.properties.balance // The decoded return value
// Check for reverts
result.revert // Error message if call reverted
// Gas information
result.estimatedGas // Gas used by the call
result.refundedGas // Gas refunded
// ABI flags (set automatically from ABI definition)
result.constant // true if function is read-only (view)
result.payable // true if function requires payment
// Events
result.events // Array of decoded events
// Access list (for optimization)
result.accessList // Storage slots accessed
```
### Using CallResult
```typescript
// Reading values - the contract throws on revert, so use try/catch
try {
const balance = await contract.balanceOf(address);
console.log('Balance:', balance.properties.balance);
} catch (error) {
console.error('Call failed:', error);
}
// Sending transactions - wrap in try/catch to handle reverts
try {
const transfer = await contract.transfer(recipient, amount, new Uint8Array(0));
const tx = await transfer.sendTransaction(params);
console.log('TX:', tx.transactionId);
} catch (error) {
console.error('Transfer failed:', error);
}
```
## Contract Methods
### Property Getters
```typescript
// Get P2OP address format
const p2opAddress = contract.p2op;
// Get contract address as Address object
const contractAddr = await contract.contractAddress;
```
### Configuration Methods
```typescript
// Set the sender for simulations
contract.setSender(myAddress);
// Set transaction details for simulation
contract.setTransactionDetails({
inputs: [],
outputs: [{ to: 'addr', value: 1000n, index: 1, flags: 0 }], // index 0 is reserved
});
// Set access list for optimization
contract.setAccessList(accessList);
// Set block height for historical queries
contract.setSimulatedHeight(12345n);
```
### Utility Methods
```typescript
// Encode calldata manually
const calldata = contract.encodeCalldata('transfer', [recipient, amount]);
// Decode events
const decodedEvents = contract.decodeEvents(rawEvents);
// Get current gas parameters
const gasParams = await contract.currentGasParameters();
```
## Best Practices
### 1. Always Handle Reverts
```typescript
// The contract throws on revert, so wrap calls in try/catch
try {
const result = await contract.someMethod(args);
// Proceed with transaction
} catch (error) {
console.error('Call would fail:', error);
return;
}
```
### 2. Use Type-Safe Interfaces
```typescript
// Good: Type-safe
const token = getContract<IOP20Contract>(addr, OP_20_ABI, provider, network);
const balance = await token.balanceOf(address); // TypeScript knows this returns balance
// Less good: No type safety
const contract = getContract(addr, OP_20_ABI, provider, network);
```
### 3. Handle BigInt Values
```typescript
// Token amounts are always bigint
const amount = 100_00000000n; // 100 tokens with 8 decimals
// Never use Number for large values
const wrong = Number(balance); // May lose precision!
```
### 4. Reuse Contract Instances
```typescript
// Good: Create once, reuse
const token = getContract<IOP20Contract>(addr, abi, provider, network);
await token.balanceOf(addr1);
await token.balanceOf(addr2);
// Bad: Creating new instances
await getContract(addr, abi, provider, network).balanceOf(addr1);
await getContract(addr, abi, provider, network).balanceOf(addr2);
```
## Next Steps
- [Instantiating Contracts](./instantiating-contracts.md) - Creating contract instances
- [Simulating Calls](./simulating-calls.md) - Read operations and simulations
- [Sending Transactions](./sending-transactions.md) - Writing to contracts
[← Previous: Advanced Configuration](../providers/advanced-configuration.md) | [Next: Instantiating Contracts →](./instantiating-contracts.md)