Skip to main content
Once you have wallet login working in your application, the next step is to integrate smart contract functionality. This guide covers everything you need to know about calling contract methods from your frontend.

Contract Interaction Basics

There are two types of contract interactions:

View Methods

Read-only operations that query contract state. These are free and don’t require user signatures.

Call Methods

State-changing operations that modify contract data. These cost gas fees and require user signatures.

Comparison Table

FeatureView MethodsCall Methods
CostFreeGas fees (~0.0001 Ⓝ)
SpeedInstant1-2 seconds
Signature RequiredNoYes
Changes StateNoYes
Can Transfer TokensNoYes

Reading Contract Data (View Methods)

View methods are used to query contract state without modifying it. They’re perfect for displaying data like balances, ownership, or any read-only information.
import { useNearWallet } from 'near-connect-hooks';
import { useEffect, useState } from 'react';

function GreetingDisplay() {
  const { viewFunction } = useNearWallet();
  const [greeting, setGreeting] = useState('');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchGreeting() {
      try {
        const result = await viewFunction({
          contractId: 'hello.near-examples.testnet',
          method: 'get_greeting',
          args: {} // No arguments needed for this method
        });
        setGreeting(result);
      } catch (error) {
        console.error('Error fetching greeting:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchGreeting();
  }, [viewFunction]);

  if (loading) return <p>Loading...</p>;
  return <p>Current greeting: {greeting}</p>;
}
View methods can be called without a wallet connection, making them perfect for displaying public contract data to all visitors.

Modifying Contract State (Call Methods)

Call methods modify contract state and require the user to sign a transaction. They cost gas fees (typically fractions of a cent) and take 1-2 seconds to complete.
import { useNearWallet } from 'near-connect-hooks';
import { useState } from 'react';

function GreetingForm() {
  const { callFunction, accountId } = useNearWallet();
  const [newGreeting, setNewGreeting] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!accountId) {
      alert('Please connect your wallet first');
      return;
    }

    setLoading(true);
    try {
      await callFunction({
        contractId: 'hello.near-examples.testnet',
        method: 'set_greeting',
        args: { greeting: newGreeting },
        gas: '30000000000000', // 30 TGas
        deposit: '0', // No deposit required
      });
      alert('Greeting updated successfully!');
    } catch (error) {
      console.error('Error updating greeting:', error);
      alert('Failed to update greeting');
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={newGreeting}
        onChange={(e) => setNewGreeting(e.target.value)}
        placeholder="Enter new greeting"
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Updating...' : 'Update Greeting'}
      </button>
    </form>
  );
}

Gas and Deposits

Understanding gas and deposits is crucial for contract interactions:

Gas

Gas is the computational fee for executing contract methods. It’s measured in TGas (teragas).
const TGAS = 1_000_000_000_000; // 1 TGas = 10^12 gas units

// Convert TGas to gas units
function toGas(tgas: number): string {
  return (tgas * TGAS).toString();
}

// Common gas amounts
const GAS_AMOUNTS = {
  simple: toGas(30),    // 30 TGas - simple operations
  medium: toGas(100),   // 100 TGas - moderate complexity
  complex: toGas(200),  // 200 TGas - complex operations
  max: toGas(300),      // 300 TGas - maximum per transaction
};
If you don’t specify gas, most libraries default to 30 TGas. Start with this and increase if transactions fail due to insufficient gas.

Deposits

Deposits are NEAR tokens attached to a transaction. They’re measured in yoctoNEAR (1 Ⓝ = 10^24 yoctoNEAR).
import { NEAR } from '@near-js/tokens';

// Convert NEAR to yoctoNEAR
function toYocto(amount: string): string {
  return NEAR.fromDecimal(amount);
}

// Convert yoctoNEAR to NEAR for display
function fromYocto(yocto: string, decimals = 2): string {
  return NEAR.fromUnits(yocto, decimals);
}

// Common deposit amounts
const DEPOSITS = {
  none: '0',
  storage: toYocto('0.01'),     // 0.01 NEAR for storage
  half: toYocto('0.5'),          // 0.5 NEAR
  one: toYocto('1'),             // 1 NEAR
  security: '1',                 // 1 yoctoNEAR for security
};
Always send amounts as strings, never as numbers. JavaScript’s Number type can’t accurately represent large integers, leading to precision loss.

Handling Data Types

Contract methods often work with special data types that need proper encoding/decoding:

Timestamps

// NEAR uses nanoseconds (19 digits)
// JavaScript Date.now() returns milliseconds (13 digits)

function jsToNearTimestamp(jsTimestamp: number): string {
  // Convert milliseconds to nanoseconds
  return (jsTimestamp * 1_000_000).toString();
}

function nearToJsTimestamp(nearTimestamp: string): number {
  // Convert nanoseconds to milliseconds
  return Math.floor(parseInt(nearTimestamp) / 1_000_000);
}

// Usage
const now = Date.now();
const nearTime = jsToNearTimestamp(now);
console.log('NEAR timestamp:', nearTime); // 19 digits

const jsTime = nearToJsTimestamp(nearTime);
const date = new Date(jsTime);
console.log('JavaScript date:', date.toISOString());

Large Numbers (U128, U64)

Contracts often use u64 or u128 for balances and IDs. JavaScript can’t handle these natively, so they’re represented as strings.
// ❌ WRONG - Will lose precision
const balance = 1000000000000000000000000; // Number

// ✅ CORRECT - Use strings
const balance = "1000000000000000000000000"; // String

// Working with large numbers
import { NEAR } from '@near-js/tokens';

// Display balance to user
function displayBalance(yoctoBalance: string) {
  const near = NEAR.fromUnits(yoctoBalance, 2);
  return `${near} NEAR`;
}

// Get balance from user input
function parseUserInput(nearAmount: string) {
  return NEAR.fromDecimal(nearAmount);
}

Account IDs

// Account IDs are strings
type AccountId = string;

// Validation helper
function isValidAccountId(accountId: string): boolean {
  // NEAR account ID rules:
  // - Lowercase letters, digits, - and _
  // - 2-64 characters
  // - Cannot start or end with separator
  const pattern = /^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$/;
  return pattern.test(accountId) && 
         accountId.length >= 2 && 
         accountId.length <= 64;
}

Error Handling

Proper error handling improves user experience and helps debug issues:
import { useNearWallet } from 'near-connect-hooks';
import { useState } from 'react';

function RobustContractCall() {
  const { callFunction, accountId } = useNearWallet();
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  const handleContractCall = async () => {
    // Reset error state
    setError(null);

    // Check wallet connection
    if (!accountId) {
      setError('Please connect your wallet first');
      return;
    }

    setLoading(true);

    try {
      await callFunction({
        contractId: 'hello.near-examples.testnet',
        method: 'set_greeting',
        args: { greeting: 'Hello!' },
        gas: '30000000000000',
        deposit: '0',
      });
    } catch (err: any) {
      // Handle different error types
      if (err.message?.includes('User rejected')) {
        setError('Transaction was cancelled');
      } else if (err.message?.includes('insufficient funds')) {
        setError('Insufficient balance to pay gas fees');
      } else if (err.message?.includes('Exceeded the prepaid gas')) {
        setError('Transaction requires more gas. Please try again.');
      } else {
        setError(`Transaction failed: ${err.message || 'Unknown error'}`);
      }
      console.error('Contract call error:', err);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <button onClick={handleContractCall} disabled={loading}>
        {loading ? 'Processing...' : 'Call Contract'}
      </button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  );
}

Batch Transactions

Send multiple transactions in a single user approval:
import { useNearWallet } from 'near-connect-hooks';

function BatchTransactionExample() {
  const { signAndSendTransactions } = useNearWallet();

  const handleBatch = async () => {
    try {
      const results = await signAndSendTransactions({
        transactions: [
          {
            receiverId: 'token.near',
            actions: [
              {
                type: 'FunctionCall',
                params: {
                  methodName: 'ft_transfer',
                  args: {
                    receiver_id: 'alice.near',
                    amount: '1000000',
                  },
                  gas: '30000000000000',
                  deposit: '1',
                },
              },
            ],
          },
          {
            receiverId: 'another.near',
            actions: [
              {
                type: 'FunctionCall',
                params: {
                  methodName: 'update_status',
                  args: { status: 'completed' },
                  gas: '30000000000000',
                  deposit: '0',
                },
              },
            ],
          },
        ],
      });

      console.log(`Completed ${results.length} transactions`);
    } catch (error) {
      console.error('Batch transaction failed:', error);
    }
  };

  return <button onClick={handleBatch}>Send Batch</button>;
}

Best Practices

function validateAndCall(amount: string, recipient: string) {
  // Validate amount
  if (!amount || parseFloat(amount) <= 0) {
    throw new Error('Invalid amount');
  }

  // Validate recipient
  if (!isValidAccountId(recipient)) {
    throw new Error('Invalid account ID');
  }

  // Proceed with transaction...
}
Users should always know when a transaction is processing:
const [txState, setTxState] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');

// Show appropriate UI based on state
{txState === 'pending' && <Spinner />}
{txState === 'success' && <SuccessMessage />}
{txState === 'error' && <ErrorMessage />}
import { useQuery } from '@tanstack/react-query';
import { useNearWallet } from 'near-connect-hooks';

function useGreeting() {
  const { viewFunction } = useNearWallet();

  return useQuery({
    queryKey: ['greeting'],
    queryFn: () => viewFunction({
      contractId: 'hello.near-examples.testnet',
      method: 'get_greeting',
    }),
    staleTime: 30000, // Cache for 30 seconds
  });
}
Always provide clear feedback when transactions fail and offer actionable next steps:
catch (error) {
  if (error.message.includes('insufficient funds')) {
    showError('Not enough NEAR to pay gas fees. Please add funds to your wallet.');
  } else {
    showError('Transaction failed. Please try again or contact support.');
  }
}
Define contract interfaces for better type safety:
interface HelloContract {
  get_greeting: () => Promise<string>;
  set_greeting: (args: { greeting: string }) => Promise<void>;
}

// Use with typed calls
const greeting = await viewFunction<string>({
  contractId: 'hello.near-examples.testnet',
  method: 'get_greeting',
});

Next Steps

Backend Authentication

Learn how to authenticate users on your backend

Build Smart Contracts

Create your own smart contracts

Token Integration

Work with fungible and non-fungible tokens

Advanced Patterns

Explore advanced integration patterns

Build docs developers (and LLMs) love